diff --git a/.github/workflows/daveai-production-e2e.yml b/.github/workflows/daveai-production-e2e.yml new file mode 100644 index 00000000000..b02bcc8bed5 --- /dev/null +++ b/.github/workflows/daveai-production-e2e.yml @@ -0,0 +1,43 @@ +name: DaveAI Production E2E + +on: + workflow_dispatch: + inputs: + base_url: + description: "DaveAI base URL" + required: false + default: "https://daveai.tech" + schedule: + - cron: "17 9 * * *" + +jobs: + production-e2e: + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + DAVEAI_BASE_URL: ${{ github.event.inputs.base_url || 'https://daveai.tech' }} + DAVEAI_E2E_EMAIL: ${{ secrets.DAVEAI_E2E_EMAIL }} + DAVEAI_E2E_PASSWORD: ${{ secrets.DAVEAI_E2E_PASSWORD }} + DAVEAI_PROOF_DIR: proofs/daveai-production-e2e + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Install Playwright runtime + run: | + npm install --no-save playwright + npx playwright install --with-deps chromium + + - name: Run DaveAI production E2E proof pack + run: node vps/production-e2e-runner.cjs + + - name: Upload E2E proofs + if: always() + uses: actions/upload-artifact@v4 + with: + name: daveai-production-e2e-proofs + path: proofs/daveai-production-e2e + if-no-files-found: warn diff --git a/.gitignore b/.gitignore index 4bc03e175d1..be31bc1678a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ pnpm-debug.log* lerna-debug.log* node_modules +__pycache__/ dist dist-ssr *.local @@ -45,4 +46,8 @@ changelogUI.md docs/instructions/Roadmap.md .cursorrules *.md +!docs/DAVEAI_PRODUCTION_E2E_AUDIT_20260731.md +!docs/DAVEAI_RC1_TO_RC2_HANDOFF_20260801.md +!docs/DAVEAI_RC2_E2E_ROADMAP_20260801.md +private/ .qodo diff --git a/docs/DAVEAI_PRODUCTION_E2E_AUDIT_20260731.md b/docs/DAVEAI_PRODUCTION_E2E_AUDIT_20260731.md new file mode 100644 index 00000000000..e6199d958a2 --- /dev/null +++ b/docs/DAVEAI_PRODUCTION_E2E_AUDIT_20260731.md @@ -0,0 +1,122 @@ +# DaveAI.tech Production E2E Audit and Roadmap — 2026-07-31 + +This document records the production truth from the July 31, 2026 DaveAI.tech audit. It separates proven-working behavior from accepted/auth-gated behavior and unresolved platform debt. + +Proof artifact root: + +`G:\Github\Bolt.DIY\proofs\daveai-prod-audit-20260731` + +## Current production status + +| Area | Status | Proof / fact | +| --- | --- | --- | +| Main DaveAI chat unauthenticated state | Working as gated | No-token and malformed-token browser proofs show auth modal, no `/api/stream` call, and no protected API storm: `09-final-no-token-auth-gate.png`, `10-final-malformed-token-auth-gate.png`. | +| Main DaveAI authenticated chat | Working | Signed-in browser proof completed with AI reply, status `Ready`, no bad API responses, no page errors: `13-final-browser-auth-chat-no429.json`, `13-final-browser-auth-chat-no429.png`. | +| Main health endpoint | Working | `https://daveai.tech/api/health` returned HTTP 200 in `29-health-endpoints.json`. | +| Catalog HTTP inventory | Working at URL level | 55 catalog entries returned HTTP 200 after login-page rate-limit fix: `15-catalog-http-audit-after-login-guard.json`. | +| Game carousel card activation | Working | Carousel cards launch selected iframe targets for Siege TD, TD2, and Asteroids: `20-carousel-iframe-proof.json`, `20-carousel-iframe-0.png`, `20-carousel-iframe-1.png`, `20-carousel-iframe-2.png`. | +| DaveAI Asteroids gameplay smoke | Working | Start/gameplay proof changed score and had no bad responses or page errors: `22-gameplay-core-proof.json`, `22-gameplay-2.png`. | +| Dave's Siege TD smoke | Working to menu | Browser proof reaches saved-name menu / campaign controls with no bad responses or page errors: `22-gameplay-core-proof.json`, `22-gameplay-0.png`. | +| DaveAI TD2 | Working after repair | Missing VFX/audio assets were repaired, stale cached asset references were cache-busted, and the final browser proof had no 404/502 responses, no request failures, no page errors, no actionable console errors, and a canvas: `24-td2-cachebust-gameplay.json`, `24-td2-cachebust-gameplay.png`. | +| Checkers Crowning | Corrected at HTTP/title level | Catalog audit shows title/H1 `Checkers Crowning`; it no longer resolves to Wheel of Fortune in the HTTP audit. | +| DaveAI Studio | Working at page-smoke level | Correct catalog URL `https://daveai.tech/studio/index.html` renders with no bad responses or console/page errors: `26-catalog-non-game-pages-smoke.json`, `26-catalog-page-studio.png`. | +| DaveAI Web Pages | Working at page-smoke level | `https://daveai.tech/web-pages.html` renders cleanly: `26-catalog-non-game-pages-smoke.json`, `26-catalog-page-web-pages.png`. | +| DaveAI Voice / Voice Landing | Working at page-smoke level | `https://voice.daveai.tech/` and `https://daveai.tech/voice-landing.html` render cleanly: `26-catalog-non-game-pages-smoke.json`, `26-catalog-page-voice.png`, `26-catalog-page-voice-landing.png`. | +| StreamHub IPTV public page | Working after repair | Public StreamHub now renders with no bad responses, no request failures, no page errors, no console errors, and a video element: `28-streamhub-after-provider-empty.json`, `28-streamhub-after-provider-empty.png`. Final combined non-game proof: `34-final-non-game-pages-smoke.json`, `34-page-streamhub.png`. | +| Windsurf / Ops page | Working as auth-gated | Catalog marks it `auth`. Public browser proof now shows an explicit auth gate and makes no protected admin API calls: `33-windsurf-auth-gate.json`, `33-windsurf-auth-gate.png`. Final combined non-game proof: `34-final-non-game-pages-smoke.json`, `34-page-windsurf.png`. | +| Provider/IPTV private provider vault | Auth-gated | Public StreamHub now gets `{"providers":[]}` at `/api/provider-vault/providers`; protected provider catalog/stream APIs still require auth. | +| `brain.daveai.tech/health` | Working after repair | Added the missing `brain.daveai.tech` Nginx vhost with the Cloudflare Origin wildcard certificate; public health now returns HTTP 200: `32-health-endpoints-after-api-fix.json`. | +| `api.daveai.tech/health` | Working after repair | Added a narrow public `/health` response while leaving the rest of the API protected; public health now returns HTTP 200: `32-health-endpoints-after-api-fix.json`. | +| `voice.daveai.tech/health` | Working after repair | Added exact `/health` proxy to the Edge TTS backend; public health now returns HTTP 200: `32-health-endpoints-after-api-fix.json`. | +| LiteLLM router | Functional; bare `/health` probe is bad | `127.0.0.1:4000/health` and model-scoped `/health?model=...` hang, but `/health/readiness`, `/health/liveliness`, `/v1/models`, and real `/v1/chat/completions` probes for `fast-agent`, `local-fallback`, and `heavy-coder` return HTTP 200. Use `vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh` for functional health until the bare LiteLLM `/health` route is fixed upstream/configured differently. Proof: `36-litellm-endpoints.txt`, `37-litellm-model-health.txt`, `38-litellm-chat-probe.txt`. | +| VPS system health | Needs maintenance | SSH banner reported high swap use, 32 zombie processes, pending updates, and restart required in `30-vps-process-health.txt`. | + +## Repeatable E2E coverage added + +Source-controlled runner: + +`vps/production-e2e-runner.cjs` + +NPM entry: + +`npm run test:daveai:prod` + +GitHub Actions workflow: + +`.github/workflows/daveai-production-e2e.yml` + +The runner covers: + +- public health endpoints, +- unauthenticated chat auth gate, +- authenticated chat reply when `DAVEAI_E2E_EMAIL` and `DAVEAI_E2E_PASSWORD` are supplied, +- carousel card launch, +- TD2/Asteroids/Siege runtime smoke, +- Studio/Web Pages/Windsurf/Voice/Voice Landing/StreamHub smoke, +- StreamHub no-error render. + +Latest public run without auth credentials: 6 passed, 0 failed, 1 skipped (`authenticated chat reply` skipped because no credentials were supplied). Proof root: + +`G:\Github\Bolt.DIY\proofs\daveai-prod-e2e-runner-20260731-rerun` + +## Repairs completed during this audit + +1. Fixed frontend auth gating and stale `Thinking...` state for unauthenticated / malformed-token chat attempts. +2. Fixed signed-in chat browser behavior by: + - using a direct native Ollama chat path in the backend, + - streaming token chunks into the existing SSE queue, + - preserving token spacing in the frontend SSE parser, + - disabling automatic chat TTS narration by default to avoid post-reply 429s. +3. Raised API and login-page rate limits enough for real browser/catalog audits without weakening credential POST throttles. +4. Fixed production game/card activation by aligning local source with production card-as-button launcher behavior. +5. Repaired DaveAI TD2 missing assets: + - 140 VFX PNG placeholders, + - 10 silent MP3 placeholders, + - cache-busted unversioned TD2 JS asset references to bypass stale CDN/origin cache. +6. Fixed StreamHub public-page provider-vault behavior by making unauthenticated `/api/provider-vault/providers` return `200 {"providers":[]}` instead of redirecting an XHR to the login domain. +7. Added public health coverage for `brain.daveai.tech`, `voice.daveai.tech`, and `api.daveai.tech`. +8. Added Windsurf/Ops auth gating so public users see a sign-in/launcher message and the page does not poll protected admin APIs until auth is present. + +## Remaining work to finish DaveAI.tech E2E + +### P0 — must finish before calling the whole platform complete + +1. Make the production changes durable in source control. + - Current VPS-only changes include backend `brain_llm.py`, Nginx configs, generated TD2 assets, TD2 built JS cache-bust, and the StreamHub Nginx provider-vault rule. + - Local repo currently includes DaveAI UI source edits and this proof/doc set, but not all VPS-only runtime changes. +2. Add repeatable E2E tests to CI or a production smoke runner. + - Chat unauthenticated gate. + - Authenticated chat reply and `Ready` status. + - Carousel card launches. + - TD2/Asteroids/Siege canvas or menu state. + - Catalog URL sweep. + - StreamHub public no-error render. + +### P1 — product quality / proof expansion + +1. Add gameplay-specific assertions for all 17 public games. Current coverage is a mix of direct smoke and deeper checks for the main three carousel games. +2. Replace TD2 placeholder VFX/audio assets with real intended assets if brand polish matters. +3. Clean up Nginx `listen 443 ssl http2` protocol-option warnings across vhosts. +4. Track LiteLLM bare `/health` separately from functional health. The router and completion path work, but bare `/health` hangs and should not be used as the production liveness check. +5. Reduce VPS operational pressure: updates, reboot window, swap pressure, zombie processes. + - Safe diagnostic script added: `vps/vps-maintenance-check.sh`. + - Do not run apt upgrades or reboot outside an explicit maintenance window. + +### P2 — catalog/status polish + +1. Define catalog status meanings: + - `live`: public route should render with no auth, no console/request failures. + - `auth`: route may require login but should not throw noisy unauthenticated browser errors. + - `historical`: retained route, not active product surface. +2. Add stale-route redirects for common wrong paths if users have already seen them: + - `/studio.html` -> `/studio/index.html` + - `/voice.html` -> `/voice-landing.html` or `https://voice.daveai.tech/` + - `/streamhub.html` -> `https://iptv.daveai.tech/` +3. Keep screenshots and JSON proofs versioned or archived with each production revision. + +## Next execution order + +1. Persist VPS-only runtime fixes into the deploy/source-of-truth repository or an explicit deployment patch directory. +2. Add the repeatable E2E smoke runner to CI or a production-proof script. +3. Run the full proof pack again from the durable source deployment path. +4. Commit, push, and open/update the PR only after the durable source matches production behavior. diff --git a/docs/DAVEAI_RC1_TO_RC2_HANDOFF_20260801.md b/docs/DAVEAI_RC1_TO_RC2_HANDOFF_20260801.md new file mode 100644 index 00000000000..dc0bb17d630 --- /dev/null +++ b/docs/DAVEAI_RC1_TO_RC2_HANDOFF_20260801.md @@ -0,0 +1,235 @@ +# DaveAI.tech RC1 → RC2 Handoff — 2026-08-01 + +This is the handoff for continuing DaveAI.tech work on a more stable coding PC with Codex. + +## Executive truth + +RC1 is not “everything forever complete,” but the production repair work is now source-controlled and reproducible. + +RC1 completed: + +- main DaveAI chat works again through the native Ollama backend path; +- unauthenticated chat attempts are cleanly auth-gated; +- TD2 missing assets and stale asset cache issue are repaired; +- game carousel launches the selected game; +- StreamHub public page no longer throws provider-vault XHR errors; +- Windsurf/Ops is now properly auth-gated and does not poll protected APIs publicly; +- public health endpoints are green; +- production runtime fixes are persisted in source under `vps/patches/daveai-production-runtime-20260731/`; +- repeatable production E2E runner exists at `vps/production-e2e-runner.cjs`; +- draft PR exists: https://github.com/stackblitz-labs/bolt.diy/pull/2186. + +RC1 remaining / RC2 target: + +- run authenticated-chat E2E with a real test account or GitHub Actions secrets; +- move from “runtime patch bundle” to the long-term canonical deployment source; +- complete deeper gameplay assertions for all public games, not just smoke/runtime checks; +- run VPS maintenance in a planned window; +- decide how to archive or publish proof artifacts without bloating the repo; +- harden LiteLLM health semantics: functional health works, bare `/health` still hangs. + +## Current branches and PRs + +Local branch: + +```text +chore/daveai-a11y-source-truth-20260725 +``` + +Remote branch: + +```text +origin/chore/daveai-a11y-source-truth-20260725 +``` + +Draft PR: + +```text +https://github.com/stackblitz-labs/bolt.diy/pull/2186 +``` + +Latest commit: + +```text +41e8b61 Persist DaveAI production runtime repairs +``` + +## Source map + +```mermaid +flowchart TD + A["Production VPS
187.77.30.206"] --> B["Runtime truth captured
2026-07-31 / 2026-08-01"] + B --> C["Source-controlled patch bundle
vps/patches/daveai-production-runtime-20260731"] + C --> D["Apply script
apply-production-runtime-fixes.sh"] + C --> E["LiteLLM functional health script
check-litellm-health.sh"] + C --> F["TD2 asset manifest + patched bundle"] + C --> G["Nginx vhost/rate-limit/health configs"] + C --> H["Windsurf auth-gated HTML"] + C --> I["brain_llm.py native streaming path"] + J["E2E runner
vps/production-e2e-runner.cjs"] --> K["Local proof artifacts
proofs/"] + J --> L["GitHub Actions
daveai-production-e2e.yml"] +``` + +## Production service map + +```mermaid +flowchart LR + User["Browser / user"] --> CF["Cloudflare"] + CF --> Nginx["VPS Nginx"] + + Nginx --> Main["daveai.tech
/var/www/agentic-website"] + Main --> Brain["agent-brain
127.0.0.1:8888"] + Brain --> Ollama["Ollama
127.0.0.1:11434"] + + Nginx --> Voice["voice.daveai.tech
edge-tts 127.0.0.1:5050"] + Nginx --> API["api.daveai.tech
public /health only; rest protected/upstream"] + Nginx --> BrainHost["brain.daveai.tech
proxy to 127.0.0.1:8888"] + Nginx --> IPTV["iptv.daveai.tech
StreamHub Docker :3103"] + IPTV --> ProviderVault["provider-vault API
protected except public empty providers list"] + Main --> Games["/games/*
TD2 / Asteroids / Siege / Arcade"] +``` + +## RC1 production proof summary + +Primary proof roots on the original PC: + +```text +G:\Github\Bolt.DIY\proofs\daveai-prod-audit-20260731 +G:\Github\Bolt.DIY\proofs\daveai-prod-e2e-runner-20260731-rerun +``` + +Latest repeatable public E2E result: + +```text +6 passed, 0 failed, 1 skipped +``` + +Skipped: + +```text +authenticated chat reply +``` + +Reason: + +```text +DAVEAI_E2E_EMAIL and DAVEAI_E2E_PASSWORD were not supplied locally. +``` + +## How to resume on another PC + +1. Copy or clone the repo. + + Preferred if using the prepared copy: + + ```powershell + cd I:\Github\Bolt.DIY + git status --short --branch + ``` + + If cloning fresh: + + ```powershell + cd I:\Github + git clone https://github.com/Ghenghis/bolt.diy.git Bolt.DIY + cd I:\Github\Bolt.DIY + git checkout chore/daveai-a11y-source-truth-20260725 + git pull + ``` + +2. Confirm GitHub CLI auth. + + ```powershell + gh auth status + gh pr view 2186 --repo stackblitz-labs/bolt.diy + ``` + +3. Install/use Node dependencies. + + If `node_modules` was copied, first try: + + ```powershell + npm run test:daveai:prod + ``` + + If dependencies are missing or stale: + + ```powershell + pnpm install + ``` + +4. Run public production E2E. + + ```powershell + $env:DAVEAI_PROOF_DIR="I:\Github\Bolt.DIY\proofs\daveai-prod-e2e-rc2" + npm run test:daveai:prod + ``` + +5. Run authenticated chat E2E when a safe test account exists. + + ```powershell + $env:DAVEAI_E2E_EMAIL="test-account@example.com" + $env:DAVEAI_E2E_PASSWORD="replace-with-private-value" + $env:DAVEAI_PROOF_DIR="I:\Github\Bolt.DIY\proofs\daveai-prod-e2e-rc2-auth" + npm run test:daveai:prod + ``` + +Do not commit real credentials. + +## RC2 acceptance gates + +RC2 should not be called complete until all of these are true: + +| Gate | Required proof | +| --- | --- | +| Public E2E | `npm run test:daveai:prod` passes with 0 failed. | +| Authenticated chat E2E | Same runner passes authenticated chat with supplied test account. | +| Durable deployment source | VPS patch bundle is either accepted as the deployment source or migrated into the canonical deploy repo/process. | +| Full game catalog | All public games have at least load/render assertions; top games have input/state assertions. | +| Health | `daveai.tech`, `brain.daveai.tech`, `voice.daveai.tech`, `api.daveai.tech`, and StreamHub provider list health checks are 200. | +| LiteLLM | `check-litellm-health.sh` passes; bare `/health` behavior documented or fixed. | +| VPS maintenance | Maintenance window run completed, reboot done if required, post-reboot smoke green. | +| Proof handling | Proof artifacts copied/archived without bloating Git history unintentionally. | + +## What not to do blindly + +- Do not run `git reset --hard` on either PC unless the target branch and local user work are explicitly confirmed. +- Do not commit `proofs/` unless intentionally creating a proof archive. +- Do not commit SSH keys, `.env`, cookies, tokens, or private credentials. +- Do not reboot or upgrade the VPS during active user traffic without a maintenance window. +- Do not replace production Nginx configs without `nginx -t` and a backup. + +## First RC2 work order + +1. On the stable PC, verify the copied checkout: + + ```powershell + git status --short --branch + npm run test:daveai:prod + ``` + +2. Add real test credentials locally or as GitHub Actions secrets: + + ```text + DAVEAI_E2E_EMAIL + DAVEAI_E2E_PASSWORD + ``` + +3. Re-run the full runner and confirm 7 passed, 0 failed, 0 skipped. +4. Run the VPS maintenance diagnostic: + + ```bash + bash vps/vps-maintenance-check.sh + ``` + +5. Schedule maintenance window for apt updates/reboot. +6. After reboot, run: + + ```bash + nginx -t + systemctl is-active nginx litellm + pm2 status + bash vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh + ``` + +7. Run `npm run test:daveai:prod` again from the stable PC. diff --git a/docs/DAVEAI_RC2_E2E_ROADMAP_20260801.md b/docs/DAVEAI_RC2_E2E_ROADMAP_20260801.md new file mode 100644 index 00000000000..0bbaa9c4332 --- /dev/null +++ b/docs/DAVEAI_RC2_E2E_ROADMAP_20260801.md @@ -0,0 +1,125 @@ +# DaveAI.tech RC2 E2E Roadmap — 2026-08-01 + +RC2 is the “prove it repeatedly” milestone. RC1 repaired production and captured the repair as source. RC2 should convert that repair into a stable, repeatable, maintenance-safe release flow. + +## RC2 completion matrix + +| Area | RC1 state | RC2 requirement | Owner action | +| --- | --- | --- | --- | +| Public E2E | 6 passed, 0 failed, 1 auth skipped | 7 passed, 0 failed, 0 skipped | Provide test credentials and rerun. | +| Chat | Public gate + signed-in manual proof works | Automated auth-chat proof in runner/CI | Add `DAVEAI_E2E_EMAIL` and `DAVEAI_E2E_PASSWORD`. | +| Games | Carousel + TD2/Asteroids/Siege smoke green | All public games catalog-smoked; primary games have input/state assertions | Extend `vps/production-e2e-runner.cjs`. | +| TD2 | Runtime repaired with placeholders and cache-bust | Replace placeholders or formally accept placeholders | Decide product-quality bar. | +| StreamHub | Public render clean | Provider-auth tests with test provider account, if available | Add private provider credentials only in secure env. | +| Windsurf | Public auth gate clean | Authenticated admin flow proof, if safe | Use admin/test token only locally. | +| LiteLLM | Functional completion works; bare `/health` hangs | Use functional probe or fix upstream health route | Keep `check-litellm-health.sh`; investigate `/health` separately. | +| VPS | Needs maintenance | Reboot/updates completed in maintenance window; post-reboot E2E green | Schedule downtime, snapshot, update, reboot, verify. | +| Docs | RC1 audit + handoff docs exist | Docs updated after every RC2 proof run | Append proof run IDs and status. | + +## E2E architecture + +```mermaid +flowchart TD + Runner["vps/production-e2e-runner.cjs"] --> Health["Health endpoints"] + Runner --> PublicChat["Unauth chat gate"] + Runner --> AuthChat["Authenticated chat reply
requires env credentials"] + Runner --> Carousel["Carousel launch"] + Runner --> Games["TD2 / Asteroids / Siege runtime"] + Runner --> Pages["Studio / Web Pages / Windsurf / Voice / StreamHub"] + Runner --> StreamHub["StreamHub no-error render"] + + Health --> Proofs["JSON + PNG proofs"] + PublicChat --> Proofs + AuthChat --> Proofs + Carousel --> Proofs + Games --> Proofs + Pages --> Proofs + StreamHub --> Proofs + + Proofs --> LocalArchive["Local proofs directory
not committed by default"] + Proofs --> ActionsArtifact["GitHub Actions artifact
when workflow runs"] +``` + +## RC2 test commands + +Public: + +```powershell +$env:DAVEAI_PROOF_DIR="I:\Github\Bolt.DIY\proofs\daveai-prod-e2e-rc2-public" +npm run test:daveai:prod +``` + +Authenticated: + +```powershell +$env:DAVEAI_E2E_EMAIL="private-test-user" +$env:DAVEAI_E2E_PASSWORD="private-test-password" +$env:DAVEAI_PROOF_DIR="I:\Github\Bolt.DIY\proofs\daveai-prod-e2e-rc2-auth" +npm run test:daveai:prod +``` + +LiteLLM: + +```bash +bash vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh +``` + +VPS maintenance diagnostic: + +```bash +bash vps/vps-maintenance-check.sh +``` + +## RC2 game proof expansion + +Minimum public game checks: + +| Game group | Minimum RC2 proof | +| --- | --- | +| Carousel games | iframe launch + direct page render + no bad responses. | +| TD2 | canvas present, no failed assets, click Play if stable, screenshot menu/game state. | +| Asteroids | click Start mission, fire/move input, score/lives/wave state remains readable. | +| Siege TD | enter name, reach menu/campaign controls, no stuck loading. | +| Older arcade games | page render, controls visible, no bad responses/page errors. | +| Checkers Crowning | verify title/H1 and not Wheel of Fortune. | + +## RC2 maintenance flow + +```mermaid +sequenceDiagram + participant User + participant StablePC as Stable coding PC + participant VPS + participant GitHub + + User->>StablePC: Pull branch / PR 2186 + StablePC->>GitHub: Verify branch and PR + StablePC->>VPS: Run read-only maintenance diagnostic + StablePC->>StablePC: Run public E2E + User->>StablePC: Provide test auth credentials + StablePC->>StablePC: Run authenticated E2E + User->>VPS: Approve maintenance window + VPS->>VPS: Snapshot, apt upgrade, reboot + StablePC->>VPS: Verify nginx, pm2, litellm, health + StablePC->>StablePC: Run full E2E again + StablePC->>GitHub: Push RC2 docs/test updates +``` + +## Known RC2 risks + +- The VPS has high swap use and a reboot-required marker. Maintenance may briefly disturb services. +- Authenticated E2E needs a safe test account; using a real admin/user account in CI is risky unless scoped and rotated. +- TD2 placeholder assets are functional but may not be final product art/audio. +- `proofs/` can grow quickly. Archive intentionally; do not blindly commit. +- Bare LiteLLM `/health` hangs even though readiness/liveliness/completions pass. + +## Definition of done for RC2 + +RC2 is done when: + +1. `npm run test:daveai:prod` returns 7 passed, 0 failed, 0 skipped with test credentials. +2. Public GitHub Actions run uploads proof artifacts. +3. VPS maintenance is completed or explicitly deferred with owner/date. +4. Post-maintenance public health and E2E are green. +5. Docs are updated with latest proof paths, dates, and exact remaining items. +6. PR is updated and ready to move from draft to review. diff --git a/package.json b/package.json index 5c8c2a977f2..ca97270e17d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "build": "remix vite:build", "dev": "node pre-start.cjs && remix vite:dev", "test": "vitest --run", + "test:daveai:prod": "node vps/production-e2e-runner.cjs", "test:watch": "vitest", "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint app", "lint:fix": "npm run lint -- --fix && prettier app --write", diff --git a/vps/DAVEAI_SOURCE_OF_TRUTH.md b/vps/DAVEAI_SOURCE_OF_TRUTH.md new file mode 100644 index 00000000000..28624bff863 --- /dev/null +++ b/vps/DAVEAI_SOURCE_OF_TRUTH.md @@ -0,0 +1,62 @@ +# DaveAI Deployment Source of Truth + +The authoritative deployment inputs are: + +- `daveai-ui-v6.html` for both the production root page and V6 page. +- `assets/daveai-v6.css` for the production workspace, responsive device + layouts, and structural layout-preset styling. +- `patches/agent-runtime-completion-20260724-src/` for the Agent Brain runtime, + Nginx route, and LiteLLM configuration. +- `daveai-sites-config.json` for the 22-site route inventory. +- `daveai-project-catalog.json` for the 55-entry project catalog. +- `services/daveai-subdomain-suite/` for DIY, Dev, Staging, Fleet, WebSocket, + and Hermes3D. +- `services/gitea/`, `services/adminer/`, and `services/monitoring/` for the + three containerized production services. + +`daveai-ui-v6.html` must remain byte-for-byte identical to the copy in the +Agent Brain runtime bundle. Its stylesheet reference must match the canonical +asset under `assets/`. Run this before any deployment: + +```powershell +python vps/verify-source-of-truth.py +node vps/verify-daveai-ui-contracts.cjs vps/daveai-ui-v6.html +node vps/verify-feature-completion-state-actions.cjs vps/daveai-ui-v6.html +``` + +The UI contract distinguishes static IDs from IDs explicitly assigned to +runtime-created nodes. It fails on unresolved references and on clickable +`div`/`span` controls that lack a role, keyboard focus, or a keyboard handler. +Dialog backdrops are exempt only when their click handler dismisses on the +backdrop itself; every dialog retains a keyboard-operable close control. + +The verified route truth is: + +- 14 `live` routes returning HTTP 200. +- 8 `auth` routes whose protected root returns the Authelia redirect. +- 0 `soon` routes. + +Do not infer `live` from the existence of a hostname or a configured port. +Update the JSON source only after an HTTP route sweep and product-specific +proof. + +The nine services completed on 2026-07-25 were accepted individually before +their labels changed. Their product evidence is recorded under task +`daveai-nine-services-completion-20260725`; the final per-service entry is +`ev_1b374693551fbcd1`. + +The canvas Code/Preview switch is a workspace-surface control. It must not +overwrite the selected global DaveAI mode. Sites `live` and `auth` labels must +also remain visibly distinct, not only present in the DOM. + +Production copies: + +- `/var/www/agentic-website/index.html` +- `/var/www/agentic-website/daveai-ui-v6.html` +- `/var/www/agentic-website/assets/daveai-v6.css` +- `/var/www/agentic-website/daveai-project-catalog.json` +- `/opt/daveai/daveai-sites-config.json` +- `/opt/daveai-subdomain-suite/` +- `/opt/daveai/services/{gitea,adminer,monitoring}/` + +Agent runtime files are installed under `/opt/agent-brain/`. diff --git a/vps/assets/daveai-v6.css b/vps/assets/daveai-v6.css new file mode 100644 index 00000000000..a149d70541c --- /dev/null +++ b/vps/assets/daveai-v6.css @@ -0,0 +1,8382 @@ +:root { + --bg-base: #020810; + --bg-panel: rgba(8, 14, 28, 0.8); + --bg-elevated: rgba(12, 18, 36, 0.97); + --border: rgba(255, 255, 255, 0.07); + --border-hi: rgba(255, 255, 255, 0.13); + --sv: #9333EA; + --sv-g: rgba(147, 51, 234, .4); + --cd: #2563EB; + --cd-g: rgba(37, 99, 235, .4); + --qa: #059669; + --qa-g: rgba(5, 150, 105, .4); + --as: #D97706; + --as-g: rgba(217, 119, 6, .4); + --accent: #8B5CF6; + --grad: linear-gradient(135deg, #9333EA, #2563EB); + --workspace-primary: #9333EA; + --workspace-secondary: #2563EB; + --workspace-primary-rgb: 147, 51, 234; + --workspace-secondary-rgb: 37, 99, 235; + --workspace-bg-rgb: 10, 13, 24; + --workspace-chat-side-w: 380px; + --t1: #F1F5F9; + --t2: #94A3B8; + --t3: #475569; + --fd: 'Syne', sans-serif; + --fb: 'DM Sans', sans-serif; + --topbar-h: 46px; + --statusbar-h: 26px; + --modebar-h: 36px; + --left-rail-w: 48px; + --right-panel-w: 360px; + --right-panel-min-w: 320px; + --right-panel-max-w: min(48vw, 640px); + --composer-h: 48px; + --voicebar-h: 28px; + --transcript-max-h: clamp(150px, 30dvh, 360px); + --coding-action-min-h: clamp(300px, 46dvh, 520px); + --coding-transcript-min-h: clamp(170px, 22dvh, 280px); + --coding-transcript-max-h: clamp(230px, 32dvh, 390px); + --action-window-min-h: 260px; + --action-window-max-w: 100%; + } + + *, + *::before, + *::after { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + background: var(--bg-base); + background-image: + radial-gradient(ellipse 55% 35% at 12% 12%, rgba(var(--workspace-primary-rgb), 0.1) 0%, transparent 55%), + radial-gradient(ellipse 45% 45% at 88% 82%, rgba(var(--workspace-secondary-rgb), 0.08) 0%, transparent 55%), + radial-gradient(#172040 1px, transparent 0); + background-size: auto, auto, 26px 26px; + color: var(--t1); + font-family: var(--fb); + height: 100vh; + height: 100dvh; + width: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + } + + .glass { + background: var(--bg-panel); + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); + border: 1px solid var(--border); + } + + .glass-hi { + background: var(--bg-elevated); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border: 1px solid var(--border-hi); + } + + .hid { + display: none !important; + } + + [role="button"][tabindex]:focus-visible, + [role="tab"][tabindex]:focus-visible, + [role="radio"][tabindex]:focus-visible, + [role="option"][tabindex]:focus-visible, + .site-item:focus-visible { + outline: 2px solid var(--workspace-primary); + outline-offset: 2px; + } + + /* ══ TOP BAR ══ */ + #topbar { + height: var(--topbar-h); + display: flex; + align-items: center; + padding: 0 10px; + gap: 6px; + border-bottom: 1px solid var(--border); + background: rgba(var(--workspace-bg-rgb), 0.95); + backdrop-filter: blur(22px); + flex-shrink: 0; + z-index: 50; + } + + .logo { + display: flex; + align-items: center; + gap: 7px; + text-decoration: none; + } + + .logo-icon { + width: 26px; + height: 26px; + border-radius: 7px; + background: var(--grad); + display: flex; + align-items: center; + justify-content: center; + font-family: var(--fd); + font-weight: 800; + font-size: 12px; + color: white; + box-shadow: 0 0 16px var(--sv-g); + flex-shrink: 0; + } + + .logo-text { + font-family: var(--fd); + font-weight: 700; + font-size: 14px; + color: var(--t1); + } + + .vdiv { + width: 1px; + height: 18px; + background: var(--border); + flex-shrink: 0; + } + + /* Agent pills */ + .agent-pills { + display: flex; + align-items: center; + gap: 3px; + } + + .pill { + display: flex; + align-items: center; + gap: 5px; + padding: 4px 8px 4px 6px; + border-radius: 18px; + font-size: 11px; + font-weight: 500; + cursor: pointer; + border: 1px solid transparent; + transition: all 0.2s; + white-space: nowrap; + } + + .pill:hover { + background: rgba(255, 255, 255, 0.04); + } + + .pill-dot { + width: 7px; + height: 7px; + border-radius: 50%; + transition: all 0.3s; + flex-shrink: 0; + } + + .pill-sub { + font-size: 9.5px; + opacity: 0.5; + margin-left: 2px; + } + + .p-sv { + color: #D8B4FE; + } + + .p-sv .pill-dot { + background: var(--sv); + } + + .p-sv:hover { + border-color: rgba(147, 51, 234, .3); + } + + .p-cd { + color: #93C5FD; + } + + .p-cd .pill-dot { + background: var(--cd); + } + + .p-cd:hover { + border-color: rgba(37, 99, 235, .3); + } + + .p-qa { + color: #6EE7B7; + } + + .p-qa .pill-dot { + background: var(--qa); + } + + .p-qa:hover { + border-color: rgba(5, 150, 105, .3); + } + + .p-as { + color: #FCD34D; + } + + .p-as .pill-dot { + background: var(--as); + } + + .p-as:hover { + border-color: rgba(217, 119, 6, .3); + } + + .s-idle .pill-dot { + opacity: 0.3; + } + + .s-think .pill-dot { + animation: pthink 1.4s ease-in-out infinite; + } + + .s-work .pill-dot { + animation: pwork .85s linear infinite; + border-radius: 2px; + } + + .s-done .pill-dot { + animation: pdone .7s ease-out forwards; + } + + @keyframes pthink { + + 0%, + 100% { + transform: scale(1); + opacity: .8; + } + + 50% { + transform: scale(1.45); + opacity: 1; + box-shadow: 0 0 8px 3px currentColor; + } + } + + @keyframes pwork { + 0% { + transform: rotate(0) scale(1.1); + border-radius: 2px; + } + + 50% { + transform: rotate(180deg) scale(.9); + border-radius: 50%; + } + + 100% { + transform: rotate(360deg) scale(1.1); + border-radius: 2px; + } + } + + @keyframes pdone { + 0% { + box-shadow: 0 0 14px 5px currentColor; + transform: scale(1.6); + } + + 100% { + box-shadow: none; + transform: scale(1); + } + } + + /* Panel tabs */ + .panel-tabs { + display: flex; + align-items: center; + gap: 2px; + } + + .ptab { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 9px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + color: var(--t3); + cursor: pointer; + border: none; + background: transparent; + transition: all .15s; + white-space: nowrap; + } + + .ptab:hover { + color: var(--t2); + background: rgba(255, 255, 255, .04); + } + + .ptab.on { + color: var(--t1); + background: rgba(255, 255, 255, .08); + } + + .ptab .badge { + background: rgba(139, 92, 246, .7); + color: white; + border-radius: 10px; + padding: 0 5px; + font-size: 9px; + line-height: 14px; + } + + /* Topbar right */ + .tb-right { + margin-left: auto; + display: flex; + align-items: center; + gap: 5px; + } + + .tb-btn { + width: 28px; + height: 28px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + color: var(--t3); + cursor: pointer; + border: none; + background: transparent; + transition: all 0.2s; + } + + .tb-btn:hover { + background: rgba(255, 255, 255, .07); + color: var(--t2); + } + + .tb-btn.on { + background: rgba(147, 51, 234, .15); + color: #D8B4FE; + } + + .layout-btn { + display: flex; + align-items: center; + gap: 4px; + padding: 0 9px; + height: 28px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + color: var(--t2); + cursor: pointer; + border: 1px solid var(--border); + background: rgba(255, 255, 255, .02); + transition: all 0.2s; + } + + .layout-btn:hover { + border-color: var(--border-hi); + color: var(--t1); + } + + .avatar { + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--grad); + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 700; + font-family: var(--fd); + cursor: pointer; + box-shadow: 0 0 12px var(--sv-g); + transition: all 0.2s; + position: relative; + } + + .avatar:hover { + transform: scale(1.08); + box-shadow: 0 0 20px var(--sv-g); + } + + .dropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + width: 170px; + border-radius: 10px; + padding: 5px; + z-index: 300; + display: none; + } + + .dropdown.open { + display: block; + animation: din .15s ease-out; + } + + @keyframes din { + from { + opacity: 0; + transform: translateY(-5px) scale(.97); + } + + to { + opacity: 1; + transform: none; + } + } + + .dd-item { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 9px; + border-radius: 7px; + font-size: 12px; + color: var(--t2); + cursor: pointer; + transition: all .15s; + } + + .dd-item:hover { + background: rgba(255, 255, 255, .06); + color: var(--t1); + } + + .dd-div { + height: 1px; + background: var(--border); + margin: 4px 0; + } + + /* ══ MAIN LAYOUT ══ */ + #main { + flex: 1; + display: flex; + overflow: hidden; + position: relative; + } + + /* ══ LEFT SIDEBAR ══ */ + #lsb { + width: var(--left-rail-w); + order: 0; + flex-shrink: 0; + background: rgba(var(--workspace-bg-rgb), 0.78); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + align-items: center; + padding: 6px 0; + gap: 2px; + z-index: 40; + } + + .sb-btn { + width: 34px; + height: 34px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: var(--t3); + cursor: pointer; + transition: all 0.2s; + position: relative; + border: none; + background: transparent; + } + + .sb-btn:hover { + background: rgba(255, 255, 255, .07); + color: var(--t2); + } + + .sb-btn.on { + background: rgba(147, 51, 234, .15); + color: #D8B4FE; + } + + .sb-btn::after { + content: attr(data-tip); + position: absolute; + left: calc(100% + 8px); + top: 50%; + transform: translateY(-50%); + background: rgba(10, 16, 32, .99); + color: var(--t1); + font-size: 11px; + font-weight: 500; + padding: 3px 8px; + border-radius: 5px; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: opacity .12s; + border: 1px solid var(--border); + z-index: 200; + } + + .sb-btn:hover::after { + opacity: 1; + } + + .sb-div { + width: 24px; + height: 1px; + background: var(--border); + margin: 3px 0; + } + + /* Device mode buttons in sidebar */ + .dev-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + width: 100%; + padding: 4px 0; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + margin: 2px 0; + } + + .dev-btn { + width: 30px; + height: 26px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + color: var(--t3); + cursor: pointer; + transition: all 0.18s; + border: none; + background: transparent; + font-size: 11px; + position: relative; + } + + .dev-btn::after { + content: attr(data-tip); + position: absolute; + left: calc(100% + 8px); + top: 50%; + transform: translateY(-50%); + background: rgba(10, 16, 32, .99); + color: var(--t1); + font-size: 11px; + font-weight: 500; + padding: 3px 8px; + border-radius: 5px; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: opacity .12s; + border: 1px solid var(--border); + z-index: 200; + } + + .dev-btn:hover::after { + opacity: 1; + } + + .dev-btn:hover { + background: rgba(255, 255, 255, .06); + color: var(--t2); + } + + .dev-btn.on { + background: rgba(37, 99, 235, .2); + color: #60A5FA; + } + + /* ══ FLY-OUT PANEL ══ */ + #pp { + position: absolute; + left: var(--left-rail-w); + top: 0; + bottom: 0; + width: 280px; + z-index: 35; + border-right: 1px solid rgba(147, 51, 234, .15); + background: rgba(5, 9, 22, 0.98); + backdrop-filter: blur(22px); + transform: translateX(-100%); + transition: transform .28s cubic-bezier(.4, 0, .2, 1); + display: flex; + flex-direction: column; + overflow-x: hidden; + overflow-y: hidden; + } + + #pp.open { + transform: translateX(0); + } + + /* Pull-tab to reveal hidden sidebar */ + #pp-pull-tab { + position: absolute; + left: var(--left-rail-w); + top: 50%; + transform: translateY(-50%); + z-index: 34; + width: 14px; + height: 56px; + background: rgba(147, 51, 234, .12); + border: 1px solid rgba(147, 51, 234, .2); + border-left: none; + border-radius: 0 8px 8px 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all .2s; + opacity: .5; + } + + #pp-pull-tab:hover { + opacity: 1; + width: 18px; + background: rgba(147, 51, 234, .25); + } + + #pp-pull-tab i { + font-size: 8px; + color: #D8B4FE; + } + + #pp-pull-tab.hidden { + display: none; + } + + /* Auto-hide timer bar at top of sidebar */ + #pp-timer-bar { + height: 2px; + background: linear-gradient(90deg, #7C3AED, #A78BFA); + width: 100%; + flex-shrink: 0; + transition: width linear; + opacity: .6; + } + + .pp-head { + padding: 10px 12px 8px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; + } + + .pp-head h3 { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .1em; + color: var(--t3); + } + + .fp-tabs { + display: flex; + gap: 2px; + padding: 6px 8px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + } + + .fp-tab { + flex: 1; + padding: 4px 6px; + border-radius: 5px; + font-size: 10.5px; + font-weight: 600; + text-align: center; + cursor: pointer; + transition: all .15s; + color: var(--t3); + border: none; + background: transparent; + } + + .fp-tab.on { + background: rgba(147, 51, 234, .18); + color: #D8B4FE; + } + + .fp-tab:hover:not(.on) { + background: rgba(255, 255, 255, .04); + color: var(--t2); + } + + .fp-body { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + } + + /* Page/Site items */ + .site-item, + .page-item { + display: flex; + align-items: center; + gap: 6px; + padding: 7px 12px; + cursor: pointer; + transition: background .12s; + font-size: 12px; + color: var(--t2); + } + + .site-item { + text-decoration: none; + } + + .site-item:hover, + .page-item:hover { + background: rgba(255, 255, 255, .04); + color: var(--t1); + } + + .site-item.on, + .page-item.on { + background: rgba(147, 51, 234, .1); + color: #D8B4FE; + border-left: 2px solid var(--sv); + padding-left: 10px; + } + + .site-dot, + .pdot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--t3); + flex-shrink: 0; + } + + .site-item.on .site-dot, + .page-item.on .pdot { + background: var(--sv); + } + + .site-badge { + margin-left: auto; + font-size: 9px; + padding: 1px 5px; + border-radius: 3px; + background: rgba(5, 150, 105, .18); + color: #6EE7B7; + } + + .pp-path { + margin-left: auto; + font-size: 9.5px; + color: var(--t3); + font-family: monospace; + } + + /* ══ PROJECTS PANEL ══ */ + .proj-cats { + display: flex; + gap: 3px; + padding: 6px 8px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + overflow-x: hidden; + } + + .proj-cat { + flex: 1; + padding: 5px 6px; + border-radius: 6px; + font-size: 10px; + font-weight: 600; + text-align: center; + cursor: pointer; + color: var(--t3); + border: none; + background: transparent; + transition: all .15s; + display: flex; + align-items: center; + justify-content: center; + gap: 3px; + white-space: nowrap; + min-width: 0; + } + + .proj-cat i { + font-size: 9px; + } + + .proj-cat.on { + background: rgba(139, 92, 246, .18); + color: #D8B4FE; + } + + .proj-cat:hover:not(.on) { + background: rgba(255, 255, 255, .04); + color: var(--t2); + } + + .proj-cat .pcat-count { + font-size: 8px; + background: rgba(255, 255, 255, .08); + border-radius: 8px; + padding: 1px 4px; + min-width: 14px; + text-align: center; + } + + .proj-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + font-size: 11px; + cursor: pointer; + color: var(--t2); + transition: all .15s; + border-left: 2px solid transparent; + } + + .proj-item:hover { + background: rgba(255, 255, 255, .04); + color: var(--t1); + } + + .proj-item.on { + background: rgba(139, 92, 246, .1); + color: #D8B4FE; + border-left-color: var(--sv); + } + + .proj-icon { + width: 28px; + height: 28px; + border-radius: 7px; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + flex-shrink: 0; + } + + .proj-icon.web { + background: rgba(96, 165, 250, .15); + color: #60A5FA; + } + + .proj-icon.apps { + background: rgba(167, 139, 250, .15); + color: #A78BFA; + } + + .proj-icon.games { + background: rgba(251, 146, 60, .15); + color: #FB923C; + } + + .proj-icon.other { + background: rgba(148, 163, 184, .15); + color: #94A3B8; + } + + .proj-meta { + flex: 1; + min-width: 0; + } + + .proj-name { + font-size: 11px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .proj-url { + font-size: 9px; + color: var(--t3); + font-family: monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .proj-badge { + font-size: 8px; + padding: 1px 5px; + border-radius: 3px; + flex-shrink: 0; + text-transform: uppercase; + font-weight: 700; + letter-spacing: .04em; + } + + .proj-badge.live { + background: rgba(5, 150, 105, .18); + color: #6EE7B7; + } + + .proj-badge.draft { + background: rgba(251, 146, 60, .18); + color: #FCD34D; + } + + .proj-del { + opacity: 0; + font-size: 9px; + color: var(--t3); + cursor: pointer; + background: none; + border: none; + padding: 2px 4px; + transition: all .15s; + pointer-events: none; + } + + .proj-item:hover .proj-del, + .proj-del:focus-visible { + opacity: 1; + pointer-events: auto; + } + + .proj-del:hover { + color: #F87171; + } + + .proj-add-form { + padding: 8px 12px; + border-top: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: 6px; + } + + .proj-add-form input, + .proj-add-form select { + width: 100%; + padding: 5px 8px; + font-size: 10.5px; + background: rgba(255, 255, 255, .04); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--t1); + font-family: 'DM Sans', sans-serif; + } + + .proj-add-form input::placeholder { + color: var(--t3); + } + + .proj-add-form select { + appearance: none; + cursor: pointer; + } + + .proj-add-btns { + display: flex; + gap: 6px; + } + + .proj-add-btns button { + flex: 1; + padding: 5px 8px; + border-radius: 6px; + font-size: 10px; + font-weight: 600; + cursor: pointer; + border: none; + font-family: 'DM Sans', sans-serif; + } + + .proj-save-btn { + background: rgba(139, 92, 246, .3); + color: #D8B4FE; + } + + .proj-save-btn:hover { + background: rgba(139, 92, 246, .5); + } + + .proj-cancel-btn { + background: rgba(255, 255, 255, .05); + color: var(--t3); + } + + .proj-cancel-btn:hover { + background: rgba(255, 255, 255, .1); + color: var(--t2); + } + + .proj-empty { + padding: 20px 14px; + text-align: center; + color: var(--t3); + font-size: 10.5px; + } + + .proj-empty i { + font-size: 20px; + display: block; + margin-bottom: 8px; + opacity: .3; + } + + /* ══ TOOLS PANEL ══ */ + .tool-search { + padding: 8px 10px 4px; + } + + .tool-search input { + width: 100%; + padding: 5px 9px; + background: rgba(255, 255, 255, .04); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--t1); + font-size: 11px; + font-family: var(--fb); + outline: none; + transition: border-color .15s; + } + + .tool-search input:focus { + border-color: rgba(147, 51, 234, .4); + } + + .tool-role-btns { + display: flex; + flex-wrap: wrap; + gap: 3px; + padding: 4px 10px 6px; + } + + .tool-runtime-status { + display: flex; + align-items: center; + justify-content: space-between; + padding: 2px 10px 6px; + color: var(--t3); + font-size: 9px; + } + + #fp-tool-count { + color: #D8B4FE; + } + + .trb { + padding: 2px 7px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + cursor: pointer; + border: none; + background: rgba(255, 255, 255, .05); + color: var(--t3); + transition: all .15s; + } + + .trb:hover { + color: var(--t2); + } + + .trb.on { + background: rgba(139, 92, 246, .25); + color: #D8B4FE; + } + + /* Tool category */ + .tool-cat { + padding: 5px 10px 3px; + font-size: 9.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .08em; + color: var(--t3); + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + user-select: none; + } + + .tool-cat:hover { + color: var(--t2); + } + + .tool-cat-count { + font-size: 9px; + background: rgba(255, 255, 255, .06); + padding: 1px 5px; + border-radius: 9px; + color: var(--t3); + } + + .tool-cat-body { + overflow: hidden; + transition: max-height .2s ease; + } + + .tool-cat-body.collapsed { + max-height: 0 !important; + } + + /* Tool item */ + .tool-item { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 6px 10px; + cursor: pointer; + transition: background .12s; + border-radius: 0; + } + + .tool-item:hover { + background: rgba(255, 255, 255, .04); + } + + .tool-item:hover .tool-run { + opacity: 1; + } + + .tool-icon { + width: 24px; + height: 24px; + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + flex-shrink: 0; + margin-top: 1px; + } + + .tool-info { + flex: 1; + min-width: 0; + } + + .tool-name { + font-size: 10.5px; + font-weight: 600; + color: var(--t1); + font-family: monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .tool-desc { + font-size: 10px; + color: var(--t3); + line-height: 1.4; + margin-top: 1px; + } + + .tool-badges { + display: flex; + gap: 3px; + margin-top: 3px; + flex-wrap: wrap; + } + + .tool-badge { + font-size: 8.5px; + padding: 1px 5px; + border-radius: 3px; + font-weight: 600; + } + + .tb-sv { + background: rgba(147, 51, 234, .18); + color: #D8B4FE; + } + + .tb-cd { + background: rgba(37, 99, 235, .18); + color: #93C5FD; + } + + .tb-qa { + background: rgba(5, 150, 105, .18); + color: #6EE7B7; + } + + .tb-as { + background: rgba(217, 119, 6, .18); + color: #FCD34D; + } + + .tool-run { + opacity: 0; + background: rgba(147, 51, 234, .25); + border: 1px solid rgba(147, 51, 234, .3); + color: #D8B4FE; + font-size: 9px; + font-weight: 600; + padding: 2px 7px; + border-radius: 4px; + cursor: pointer; + transition: all .15s; + flex-shrink: 0; + margin-top: 2px; + } + + .tool-run:hover { + background: rgba(147, 51, 234, .4); + } + + /* Agent item */ + .agent-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-bottom: 1px solid rgba(255, 255, 255, .03); + } + + .ag-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + + .ag-role { + font-size: 11px; + font-weight: 600; + width: 62px; + } + + .ag-task { + font-size: 10px; + color: var(--t3); + flex: 1; + } + + .ag-state { + font-size: 9px; + padding: 2px 6px; + border-radius: 9px; + background: rgba(255, 255, 255, .06); + color: var(--t3); + } + + /* ══ CANVAS ══ */ + #canvas { + flex: 1; + order: 1; + min-width: 0; + display: flex; + flex-direction: column; + overflow: hidden; + background: rgba(var(--workspace-bg-rgb), 0.52); + position: relative; + } + + #ctb { + height: var(--modebar-h); + display: flex; + align-items: center; + padding: 0 8px; + gap: 3px; + border-bottom: 1px solid var(--border); + background: rgba(3, 6, 16, 0.8); + flex-shrink: 0; + } + + .ctab { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 9px; + border-radius: 5px; + font-size: 10.5px; + font-weight: 600; + color: var(--t3); + cursor: pointer; + transition: all .15s; + white-space: nowrap; + border: none; + background: transparent; + } + + .ctab:hover { + color: var(--t2); + background: rgba(255, 255, 255, .04); + } + + .ctab.on { + color: var(--t1); + background: rgba(255, 255, 255, .09); + } + + .ctab.dev-mode { + padding: 4px 7px; + } + + /* device mode indicator */ + .dm-indicator { + display: flex; + align-items: center; + gap: 5px; + margin-left: 6px; + padding: 3px 8px; + background: rgba(37, 99, 235, .1); + border: 1px solid rgba(37, 99, 235, .2); + border-radius: 12px; + font-size: 9.5px; + color: #60A5FA; + font-weight: 600; + } + + .curl { + font-size: 10px; + color: var(--t3); + font-family: monospace; + display: flex; + align-items: center; + gap: 4px; + padding: 0 6px; + } + + #panel-area { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + position: relative; + min-height: var(--action-window-min-h); + } + + #bframe { + flex: 1; + display: flex; + flex-direction: column; + margin: 8px 8px 0; + border-radius: 10px; + overflow: hidden; + border: 1px solid var(--border); + box-shadow: 0 0 50px rgba(0, 0, 0, .55); + background: #08101E; + position: relative; + min-width: 0; + min-height: var(--action-window-min-h); + max-width: var(--action-window-max-w); + transition: width .3s ease, margin .3s ease; + } + + /* Device mode frames */ + .dm-desktop #bframe { + align-self: stretch; + flex: 1 1 auto; + width: calc(100% - 16px); + max-width: calc(100% - 16px); + margin: 8px 8px 0; + } + + .dm-tablet #bframe { + align-self: center; + flex: 1 1 auto; + min-width: 0; + width: 768px; + max-width: calc(100% - 16px); + margin: 8px auto 0; + } + + .dm-android #bframe { + align-self: center; + flex: 1 1 auto; + min-width: 0; + width: 412px; + max-width: calc(100% - 16px); + margin: 8px auto 0; + } + + .dm-mobile #bframe { + align-self: center; + flex: 1 1 auto; + min-width: 0; + width: 375px; + max-width: calc(100% - 16px); + margin: 8px auto 0; + } + + /* Device chrome ring */ + .dm-tablet .frame-chrome, + .dm-android .frame-chrome, + .dm-mobile .frame-chrome { + display: flex; + } + + .frame-chrome { + display: none; + align-items: center; + justify-content: center; + height: 28px; + background: rgba(255, 255, 255, .03); + border-bottom: 1px solid var(--border); + flex-shrink: 0; + } + + .fc-notch { + width: 60px; + height: 6px; + background: rgba(255, 255, 255, .12); + border-radius: 3px; + } + + /* Preview inner */ + .btb { + height: 32px; + background: rgba(6, 10, 24, 0.9); + display: flex; + align-items: center; + padding: 0 10px; + gap: 8px; + flex-shrink: 0; + } + + .bdots { + display: flex; + gap: 5px; + align-items: center; + } + + .bd { + width: 9px; + height: 9px; + border-radius: 50%; + } + + .bd-r { + background: #EF4444; + opacity: .6; + } + + .bd-y { + background: #EAB308; + opacity: .6; + } + + .bd-g { + background: #22C55E; + opacity: .6; + } + + .burl { + flex: 1; + text-align: center; + font-size: 10px; + color: var(--t3); + font-family: monospace; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + } + + #lp { + flex: 1; + overflow-y: auto; + background: linear-gradient(160deg, #050B1A 0%, #080F22 60%, #040A16 100%); + position: relative; + } + + #pc { + display: flex; + flex-direction: column; + align-items: center; + padding: 48px 24px 40px; + min-height: 100%; + } + + /* preview page elements */ + .ph { + text-align: center; + max-width: 620px; + } + + .p-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 20px; + border: 1px solid rgba(147, 51, 234, .3); + background: rgba(147, 51, 234, .08); + font-size: 10.5px; + font-weight: 600; + color: #D8B4FE; + margin-bottom: 16px; + cursor: pointer; + } + + .p-title { + font-family: var(--fd); + font-size: clamp(28px, 4.5vw, 52px); + font-weight: 800; + background: linear-gradient(135deg, #F1F5F9 30%, #94A3B8 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; + line-height: 1.12; + margin-bottom: 14px; + cursor: pointer; + } + + .p-sub { + font-size: clamp(13px, 1.5vw, 16px); + color: #4E6070; + line-height: 1.65; + max-width: 500px; + margin: 0 auto 26px; + cursor: pointer; + } + + .p-acts { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; + } + + .pb1 { + padding: 11px 28px; + border-radius: 10px; + background: var(--grad); + color: white; + font-weight: 700; + font-size: 13.5px; + border: none; + cursor: pointer; + box-shadow: 0 0 28px rgba(147, 51, 234, .45); + transition: all .2s; + } + + .pb1:hover { + transform: translateY(-1px); + box-shadow: 0 0 40px rgba(147, 51, 234, .6); + } + + .pb2 { + padding: 11px 28px; + border-radius: 10px; + background: rgba(255, 255, 255, .05); + color: var(--t2); + font-weight: 600; + font-size: 13.5px; + border: 1px solid var(--border); + cursor: pointer; + transition: all .2s; + } + + .pb2:hover { + border-color: var(--border-hi); + color: var(--t1); + } + + .ed { + transition: outline .15s; + outline: 2px solid transparent; + outline-offset: 3px; + } + + .ed:hover { + outline-color: rgba(147, 51, 234, .5); + } + + .hf { + animation: hfl .85s ease-out; + } + + @keyframes hfl { + 0% { + outline-color: rgba(139, 92, 246, .9); + } + + 100% { + outline-color: transparent; + } + } + + @keyframes fiu { + from { + opacity: 0; + transform: translateY(14px); + } + + to { + opacity: 1; + transform: none; + } + } + + /* Build status bar */ + #bst { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 4px 12px; + font-size: 10px; + font-weight: 600; + color: #6EE7B7; + background: rgba(5, 150, 105, .1); + border-top: 1px solid rgba(5, 150, 105, .15); + display: none; + align-items: center; + gap: 6px; + } + + /* Panel content */ + .panel-content { + flex: 1; + overflow-y: auto; + display: none; + flex-direction: column; + animation: panelFadeIn .2s ease; + } + + .panel-content.vis { + display: flex !important; + } + + @keyframes panelFadeIn { + from { + opacity: 0; + transform: translateY(4px); + } + + to { + opacity: 1; + transform: none; + } + } + + .pc-header { + padding: 11px 14px; + border-bottom: 1px solid var(--border); + font-size: 12px; + font-weight: 700; + color: var(--t2); + display: flex; + align-items: center; + gap: 7px; + flex-shrink: 0; + } + + /* Build/Analytics/Admin panels */ + .bt-item { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 8px 14px; + border-bottom: 1px solid rgba(255, 255, 255, .03); + } + + .bt-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + margin-top: 3px; + } + + .bt-status { + display: inline-flex; + align-items: center; + font-size: 9.5px; + padding: 1px 6px; + border-radius: 3px; + font-weight: 700; + background: rgba(255, 255, 255, .06); + color: var(--t3); + margin-bottom: 2px; + } + + .bt-status.pass { + background: rgba(5, 150, 105, .15); + color: #6EE7B7; + } + + .bt-meta { + font-size: 9.5px; + color: var(--t3); + margin-top: 2px; + font-family: monospace; + } + + .bt-meta.bt-commit { + color: var(--qa); + } + + .ana-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + padding: 12px 14px; + } + + .ana-card { + background: rgba(255, 255, 255, .03); + border: 1px solid var(--border); + border-radius: 8px; + padding: 10px 12px; + } + + .ana-label { + font-size: 10px; + color: var(--t3); + margin-bottom: 4px; + } + + .ana-val { + font-size: 22px; + font-weight: 800; + font-family: var(--fd); + line-height: 1; + } + + .ana-sub { + font-size: 9.5px; + color: var(--t3); + margin-top: 3px; + } + + .ana-link { + display: flex; + align-items: center; + gap: 7px; + padding: 7px 14px; + font-size: 11px; + color: var(--t2); + border-bottom: 1px solid rgba(255, 255, 255, .03); + text-decoration: none; + transition: color .15s; + } + + .ana-link:hover { + color: var(--t1); + background: rgba(255, 255, 255, .03); + } + + .adm-tabs { + display: flex; + gap: 4px; + padding: 8px 14px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + } + + .adm-tab { + padding: 4px 10px; + border-radius: 5px; + font-size: 11px; + font-weight: 600; + color: var(--t3); + cursor: pointer; + border: none; + background: transparent; + transition: all .15s; + } + + .adm-tab.on { + background: rgba(147, 51, 234, .18); + color: #D8B4FE; + } + + .adm-section { + display: flex; + flex-direction: column; + } + + .adm-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 6px 0; + border-bottom: 1px solid rgba(255, 255, 255, .04); + font-size: 11px; + } + + .adm-row:last-child { + border-bottom: none; + } + + .adm-row span { + color: var(--t3); + } + + .adm-row code { + font-family: monospace; + font-size: 10px; + color: var(--t2); + background: rgba(255, 255, 255, .05); + padding: 1px 5px; + border-radius: 3px; + } + + /* Chat */ + #chat { + flex-shrink: 0; + display: flex; + flex-direction: column; + min-height: 0; + max-height: calc(var(--transcript-max-h) + var(--voicebar-h) + var(--composer-h) + 16px); + border-top: 1px solid var(--border); + background: rgba(3, 6, 16, 0.85); + padding: 0 0 6px; + overflow: hidden; + } + + #chat-feed { + flex: 0 1 auto; + max-height: var(--transcript-max-h) !important; + min-height: 0; + overflow-y: auto !important; + overflow-x: hidden; + box-sizing: border-box; + padding-bottom: 12px; + scroll-padding-bottom: 12px; + } + + #chat-feed > .cb:last-child { + margin-bottom: 4px; + } + + #chat-feed .cb { + max-width: min(100%, 1120px); + overflow: visible; + } + + #chat-feed .cb-inner { + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; + line-height: 1.55; + max-height: none; + } + + #chat-feed .cb-artifact-summary .cb-inner { + border-color: rgba(96, 165, 250, .22) !important; + background: rgba(37, 99, 235, .08) !important; + color: var(--t2); + font-size: 11.5px; + } + + body.has-chat-transcript #panel-area { + min-height: var(--coding-action-min-h); + } + + body.has-chat-transcript #bframe { + min-height: calc(var(--coding-action-min-h) - 10px); + } + + body.has-chat-transcript #chat { + max-height: min(44dvh, calc(var(--coding-transcript-max-h) + 120px)); + } + + body.has-chat-transcript #chat-feed { + display: flex !important; + min-height: var(--coding-transcript-min-h); + max-height: var(--coding-transcript-max-h) !important; + } + + body.has-chat-transcript #chat-feed:empty { + display: none !important; + min-height: 0; + max-height: 0 !important; + } + + #ts { + padding: 6px 12px; + border-bottom: 1px solid rgba(255, 255, 255, .04); + display: none; + align-items: center; + gap: 8px; + font-size: 11px; + } + + .ta { + font-weight: 700; + } + + #ts.vis { + display: flex; + } + + .ts-ic { + color: var(--sv); + opacity: .7; + font-size: 11px; + } + + #cb { + display: flex; + align-items: center; + gap: 5px; + padding: 8px 10px 4px; + width: min(calc(100% - 20px), 1120px); + max-width: 1120px; + min-height: var(--composer-h); + box-sizing: border-box; + margin-right: auto; + } + + #tc { + display: none; + align-items: center; + gap: 5px; + padding: 3px 8px; + border-radius: 5px; + background: rgba(147, 51, 234, .15); + border: 1px solid rgba(147, 51, 234, .25); + font-size: 10px; + color: #D8B4FE; + cursor: pointer; + white-space: nowrap; + } + + #tc.on { + display: flex; + } + + #pi { + flex: 1; + background: rgba(255, 255, 255, .04); + border: 1px solid var(--border); + border-radius: 8px; + padding: 8px 12px; + color: var(--t1); + font-size: 12.5px; + font-family: var(--fb); + outline: none; + resize: none; + min-height: 36px; + max-height: 120px; + overflow-y: auto; + line-height: 1.4; + transition: border-color .15s; + } + + #pi:focus { + border-color: rgba(147, 51, 234, .4); + } + + .cib { + width: 30px; + height: 30px; + border-radius: 7px; + background: none; + border: none; + color: var(--t3); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all .2s; + } + + .cib:hover { + color: var(--t2); + background: rgba(255, 255, 255, .06); + } + + .cib.mic-on { + color: #F87171; + background: rgba(239, 68, 68, .15); + box-shadow: 0 0 12px rgba(239, 68, 68, .4), 0 0 24px rgba(239, 68, 68, .15); + animation: mic-pulse 1.2s ease-in-out infinite; + position: relative; + } + + .cib.mic-on::after { + content: ''; + position: absolute; + inset: -3px; + border-radius: 10px; + border: 2px solid rgba(239, 68, 68, .3); + animation: mic-ring 1.2s ease-in-out infinite; + pointer-events: none; + } + + @keyframes mic-pulse { + + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: .7; + transform: scale(1.05); + } + } + + @keyframes mic-ring { + + 0%, + 100% { + transform: scale(1); + opacity: .5; + } + + 50% { + transform: scale(1.15); + opacity: 0; + } + } + + /* Agent typing indicator in chat */ + .agent-typing { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + font-size: 10.5px; + color: var(--t3); + animation: fiu .3s ease; + } + + .agent-typing-name { + font-weight: 700; + } + + .agent-typing-name.sv { + color: #D8B4FE; + } + + .agent-typing-name.cd { + color: #93C5FD; + } + + .agent-typing-name.qa { + color: #6EE7B7; + } + + .agent-typing-name.as { + color: #FCD34D; + } + + .typing-dots { + display: flex; + gap: 3px; + align-items: center; + } + + .typing-dots span { + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--t3); + animation: typeDot 1.4s ease-in-out infinite; + } + + .typing-dots span:nth-child(2) { + animation-delay: .2s; + } + + .typing-dots span:nth-child(3) { + animation-delay: .4s; + } + + @keyframes typeDot { + + 0%, + 60%, + 100% { + opacity: .3; + transform: translateY(0); + } + + 30% { + opacity: 1; + transform: translateY(-3px); + } + } + + /* Chat message styling */ + .chat-msg { + padding: 6px 12px; + font-size: 11px; + line-height: 1.5; + animation: fiu .3s ease; + border-bottom: 1px solid rgba(255, 255, 255, .03); + } + + .chat-msg-user { + color: var(--t1); + background: rgba(147, 51, 234, .06); + } + + .chat-msg-agent { + color: var(--t2); + } + + .chat-msg-agent .chat-agent-name { + font-weight: 700; + font-size: 9.5px; + margin-bottom: 2px; + } + + @keyframes sbPulse { + + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: .3; + transform: scale(.6); + } + } + + .csep { + width: 1px; + height: 18px; + background: var(--border); + } + + .sbtn { + height: 30px; + padding: 0 14px; + border-radius: 7px; + background: var(--grad); + color: white; + font-size: 12px; + font-weight: 700; + border: none; + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; + transition: all .2s; + } + + .sbtn:hover { + opacity: .9; + transform: translateY(-1px); + } + + #sugs { + display: flex; + gap: 5px; + padding: 0 10px 4px; + flex-wrap: wrap; + width: min(calc(100% - 20px), 1120px); + max-width: 1120px; + box-sizing: border-box; + margin-right: auto; + } + + .sug { + padding: 3px 10px; + border-radius: 12px; + background: rgba(255, 255, 255, .04); + border: 1px solid var(--border); + font-size: 10.5px; + color: var(--t2); + cursor: pointer; + transition: all .15s; + white-space: nowrap; + } + + .sug:hover { + border-color: var(--border-hi); + color: var(--t1); + } + + /* Right panel */ + #rp { + width: var(--right-panel-w); + order: 2; + min-width: var(--right-panel-min-w); + max-width: var(--right-panel-max-w); + flex-shrink: 0; + background: rgba(var(--workspace-bg-rgb), 0.92); + border-left: 1px solid var(--border); + display: flex; + flex-direction: column; + position: relative; + transition: width .25s ease; + overflow: hidden; + } + + #rp.col, + #rp.collapsed { + width: 0; + min-width: 0; + max-width: 0; + border-left: none; + } + + #rp.is-resizing { + transition: none; + } + + body.rp-resizing { + cursor: col-resize; + user-select: none; + } + + #rp-resizer { + position: absolute; + z-index: 12; + top: 0; + bottom: 0; + left: 0; + width: 8px; + cursor: col-resize; + touch-action: none; + outline: none; + } + + #rp-resizer::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 2px; + background: transparent; + transition: background .15s ease, box-shadow .15s ease; + } + + #rp-resizer:hover::after, + #rp-resizer:focus-visible::after, + #rp.is-resizing #rp-resizer::after { + background: #8B5CF6; + box-shadow: 0 0 10px rgba(139, 92, 246, .55); + } + + .rph { + display: flex; + align-items: center; + padding: 8px 10px; + border-bottom: 1px solid var(--border); + gap: 6px; + flex-shrink: 0; + min-width: 0; + } + + .rp-tabs { + display: flex; + gap: 3px; + flex: 1 1 auto; + min-width: 0; + } + + .rp-tab { + padding: 3px 8px; + border-radius: 5px; + font-size: 11px; + font-weight: 600; + color: var(--t3); + cursor: pointer; + transition: all .15s; + } + + .rp-tab.on { + color: var(--t1); + background: rgba(255, 255, 255, .06); + } + + .rp-mode-toggle { + display: flex; + align-items: center; + gap: 2px; + flex: 0 0 auto; + } + + .rp-close { + appearance: none; + width: 22px; + height: 22px; + flex: 0 0 22px; + display: grid; + place-items: center; + border: 0; + border-radius: 5px; + background: transparent; + color: var(--t3); + cursor: pointer; + } + + .rp-close:hover, + .rp-close:focus-visible { + background: rgba(255, 255, 255, .06); + color: var(--t1); + outline: none; + } + + /* ── Memory pane (Wave 4 Phase D) — mobile inherits #rp (≤600px collapse) ── */ + #rp-memory-pane { flex-direction:column; flex:1; overflow:hidden } + #rpm-list { flex:1; overflow-y:auto; padding:4px 0 } + #rpm-empty { padding:20px 14px; text-align:center; font-size:11px; color:var(--t3); line-height:1.55 } + #rpm-empty .rpm-empty-icon { font-size:22px; display:block; margin-bottom:8px; opacity:.35 } + #rpm-empty button { margin-top:10px; background:rgba(167,139,250,.12); border:1px solid rgba(167,139,250,.35); color:#c4b5fd; padding:5px 12px; border-radius:12px; font-size:10px; font-weight:600; cursor:pointer; font-family:inherit } + #rpm-empty button:hover { background:rgba(167,139,250,.22) } + .rpm-card { margin:6px 8px; padding:9px 10px 8px; background:rgba(255,255,255,.04); border-radius:8px; border-left:3px solid var(--rpm-type-color,#8B5CF6); transition:background .15s ease, transform .15s ease; animation:fadeIn .2s ease } + .rpm-card:hover { background:rgba(255,255,255,.07); transform:translateY(-1px) } + .rpm-card[data-type="project"] { --rpm-type-color:#8B5CF6; --rpm-type-tint:rgba(139,92,246,.14) } + .rpm-card[data-type="chat"], .rpm-card[data-type="session"] { --rpm-type-color:#A78BFA; --rpm-type-tint:rgba(167,139,250,.14) } + .rpm-card[data-type="build"] { --rpm-type-color:#EC4899; --rpm-type-tint:rgba(236,72,153,.14) } + .rpm-card[data-type="edit"] { --rpm-type-color:#60A5FA; --rpm-type-tint:rgba(96,165,250,.14) } + .rpm-row-top { display:flex; align-items:center; gap:6px; margin-bottom:4px } + .rpm-badge { font-size:8px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; padding:2px 7px; border-radius:9px; color:var(--rpm-type-color); background:var(--rpm-type-tint); flex-shrink:0 } + .rpm-loc-icon { font-size:11px; line-height:1; flex-shrink:0; opacity:.85 } + .rpm-title { font-size:11.5px; font-weight:600; color:var(--t1); line-height:1.35; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; flex:1; min-width:0; word-break:break-word } + .rpm-meta { display:flex; align-items:center; gap:6px; font-size:9.5px; color:var(--t3); margin:3px 0 6px; flex-wrap:wrap } + .rpm-status { display:inline-flex; align-items:center; padding:1px 6px; border-radius:8px; font-size:8.5px; font-weight:600; text-transform:uppercase; letter-spacing:.04em } + .rpm-status.live, .rpm-status.active { color:#6EE7B7; background:rgba(110,231,183,.12) } + .rpm-status.draft { color:#FCD34D; background:rgba(252,211,77,.12) } + .rpm-status.archived { color:#94a3b8; background:rgba(148,163,184,.12) } + .rpm-actions { display:flex; gap:5px; margin-top:2px } + .rpm-act { padding:3px 9px; border-radius:10px; font-size:9.5px; font-weight:600; border:1px solid rgba(255,255,255,.14); background:rgba(255,255,255,.04); color:var(--t1); cursor:pointer; font-family:inherit; transition:background .12s ease, border-color .12s ease } + .rpm-act:hover { background:rgba(255,255,255,.12); border-color:rgba(255,255,255,.25) } + .rpm-act.primary { background:var(--rpm-type-tint); border-color:var(--rpm-type-color); color:var(--rpm-type-color) } + .rpm-act.primary:hover { background:var(--rpm-type-color); color:#0a0e1a } + .rpm-skel { margin:6px 8px; padding:10px 12px; background:rgba(255,255,255,.03); border-radius:8px; border-left:3px solid rgba(167,139,250,.25) } + .rpm-skel-bar { height:8px; background:linear-gradient(90deg,rgba(255,255,255,.04) 0%,rgba(255,255,255,.10) 50%,rgba(255,255,255,.04) 100%); background-size:200% 100%; border-radius:4px; margin-bottom:6px; animation:rpm-shimmer 1.4s infinite linear } + .rpm-skel-bar.w50 { width:55% } .rpm-skel-bar.w30 { width:35% } .rpm-skel-bar.w80 { width:80% } + @keyframes rpm-shimmer { 0% { background-position:200% 0 } 100% { background-position:-200% 0 } } + + /* Activity feed */ + #af { + flex: 1; + overflow-y: auto; + padding: 4px 0; + } + + .ai { + padding: 7px 10px; + border-bottom: 1px solid rgba(255, 255, 255, .03); + } + + .ai-hd { + display: flex; + align-items: center; + gap: 5px; + margin-bottom: 3px; + } + + .ai-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + } + + .ai-name { + font-size: 10px; + font-weight: 700; + } + + .ai-ts { + font-size: 9.5px; + color: var(--t3); + margin-left: auto; + } + + .ai-txt { + font-size: 11px; + color: var(--t2); + } + + .ai-cm { + font-size: 9.5px; + font-family: monospace; + color: var(--t3); + margin-top: 2px; + background: rgba(255, 255, 255, .03); + padding: 1px 6px; + border-radius: 3px; + display: inline-block; + } + + .rp-met { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; + padding: 8px 10px; + border-top: 1px solid var(--border); + } + + .mc { + background: rgba(255, 255, 255, .03); + border-radius: 6px; + padding: 7px 9px; + } + + .ml { + font-size: 9px; + color: var(--t3); + margin-bottom: 3px; + text-transform: uppercase; + letter-spacing: .05em; + } + + .mv { + font-size: 18px; + font-weight: 800; + font-family: var(--fd); + line-height: 1; + } + + .ms { + font-size: 9px; + color: var(--t3); + margin-top: 2px; + } + + /* History */ + .hist-item { + padding: 7px 10px; + border-bottom: 1px solid rgba(255, 255, 255, .03); + cursor: pointer; + transition: background .12s; + } + + .hist-item:hover { + background: rgba(255, 255, 255, .04); + } + + .hist-prompt { + font-size: 11px; + color: var(--t2); + } + + .hist-meta { + font-size: 9.5px; + color: var(--t3); + margin-top: 2px; + } + + /* Tool output modal */ + #tool-modal { + position: fixed; + inset: 0; + z-index: 500; + display: none; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, .65); + backdrop-filter: blur(6px); + } + + #tool-modal.open { + display: flex; + } + + .tm-box { + width: min(600px, 92vw); + max-height: 80vh; + border-radius: 14px; + display: flex; + flex-direction: column; + overflow: hidden; + animation: din .18s ease-out; + } + + .tm-head { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + } + + .tm-title { + font-family: monospace; + font-size: 13px; + font-weight: 700; + color: var(--t1); + flex: 1; + } + + .tm-badge { + font-size: 9px; + padding: 2px 7px; + border-radius: 4px; + font-weight: 600; + } + + .tm-close { + background: none; + border: none; + color: var(--t3); + cursor: pointer; + font-size: 14px; + padding: 2px; + } + + .tm-close:hover { + color: var(--t1); + } + + .tm-body { + flex: 1; + overflow-y: auto; + padding: 14px 16px; + font-family: monospace; + font-size: 11.5px; + color: #7DD3FC; + line-height: 1.7; + white-space: pre-wrap; + word-break: break-all; + } + + .tm-foot { + padding: 10px 16px; + border-top: 1px solid var(--border); + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + } + + .tm-input { + flex: 1; + background: rgba(255, 255, 255, .05); + border: 1px solid var(--border); + border-radius: 7px; + padding: 6px 10px; + color: var(--t1); + font-family: monospace; + font-size: 11px; + outline: none; + } + + .tm-input:focus { + border-color: rgba(147, 51, 234, .4); + } + + .tm-run-btn { + padding: 6px 14px; + border-radius: 7px; + background: rgba(147, 51, 234, .3); + border: 1px solid rgba(147, 51, 234, .4); + color: #D8B4FE; + font-size: 11px; + font-weight: 600; + cursor: pointer; + transition: all .15s; + } + + .tm-run-btn:hover { + background: rgba(147, 51, 234, .5); + } + + /* API status dot */ + .api-status { + display: flex; + align-items: center; + gap: 4px; + font-size: 10px; + color: var(--t3); + padding: 0 6px; + } + + .api-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #EF4444; + } + + .api-dot.online { + background: #22C55E; + animation: pthink 2s ease-in-out infinite; + } + + /* Layout picker */ + #lp-ov { + position: fixed; + inset: 0; + z-index: 400; + background: rgba(2, 6, 18, .88); + backdrop-filter: blur(16px); + display: flex; + align-items: center; + justify-content: center; + padding: clamp(18px, 3vw, 42px); + overflow-y: auto; + } + + .lp-shell { + position: relative; + width: min(1180px, 96vw); + max-height: calc(100dvh - 48px); + display: flex; + flex-direction: column; + padding: clamp(22px, 3vw, 34px); + border: 1px solid rgba(var(--workspace-primary-rgb), .22); + border-radius: 20px; + background: + linear-gradient(145deg, rgba(var(--workspace-primary-rgb), .08), transparent 35%), + rgba(var(--workspace-bg-rgb), .97); + box-shadow: 0 28px 90px rgba(0, 0, 0, .58); + } + + .lp-x { + position: absolute; + top: 14px; + right: 14px; + width: 32px; + height: 32px; + display: grid; + place-items: center; + border: 1px solid var(--border); + border-radius: 8px; + background: rgba(255, 255, 255, .035); + color: var(--t2); + cursor: pointer; + } + + .lp-x:hover, + .lp-x:focus-visible { + border-color: rgba(var(--workspace-primary-rgb), .45); + color: var(--t1); + outline: none; + } + + .lph { + text-align: center; + margin-bottom: 22px; + padding-inline: 44px; + } + + .lph h2 { + font-family: var(--fd); + font-size: clamp(22px, 2.4vw, 30px); + font-weight: 800; + margin-bottom: 8px; + } + + .lph p { + font-size: 12.5px; + color: var(--t2); + } + + .lgrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); + gap: 12px; + width: 100%; + max-height: min(62dvh, 620px); + overflow-y: auto; + padding: 5px 7px 10px 5px; + scrollbar-color: rgba(var(--workspace-primary-rgb), .45) transparent; + } + + .lcard { + appearance: none; + min-width: 0; + overflow: hidden; + border: 1px solid var(--border-hi); + border-radius: 12px; + background: rgba(255, 255, 255, .025); + color: inherit; + font-family: inherit; + text-align: left; + overflow: hidden; + cursor: pointer; + transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease, background .18s ease; + } + + .lcard:hover, + .lcard.sel { + transform: translateY(-2px); + border-color: var(--workspace-primary); + background: rgba(var(--workspace-primary-rgb), .07); + box-shadow: 0 10px 28px rgba(0, 0, 0, .28), 0 0 20px rgba(var(--workspace-primary-rgb), .16); + } + + .lcard:focus-visible { + outline: 2px solid var(--workspace-primary); + outline-offset: 2px; + } + + .lthumb { + height: 96px; + position: relative; + overflow: hidden; + padding: 6px; + border-bottom: 1px solid rgba(255, 255, 255, .06); + } + + .lt-bar { + height: 8px; + margin-bottom: 4px; + border-radius: 4px; + opacity: .62; + } + + .lt-workspace { + height: calc(100% - 12px); + min-width: 0; + display: flex; + gap: 3px; + } + + .lt-rail, + .lt-activity, + .lt-side-chat, + .lt-canvas, + .lt-chat { + border: 1px solid rgba(255, 255, 255, .1); + border-radius: 3px; + opacity: .3; + } + + .lt-rail, + .lt-activity, + .lt-side-chat { + flex: 0 0 auto; + } + + .lt-center { + min-width: 0; + flex: 1; + display: flex; + flex-direction: column; + gap: 3px; + } + + .lt-canvas { + min-height: 0; + flex: 1; + opacity: .2; + } + + .lt-chat { + flex: 0 0 auto; + } + + .lmeta { + padding: 9px 10px 10px; + } + + .lname { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 0; + font-size: 11px; + font-weight: 800; + color: var(--t1); + } + + .lname i { + display: none; + color: var(--workspace-primary); + font-size: 10px; + } + + .lcard.sel .lname i { + display: inline-block; + } + + .ldesc { + min-height: 30px; + margin-top: 5px; + font-size: 9.5px; + line-height: 1.45; + color: var(--t2); + } + + .lp-actions { + display: flex; + align-items: center; + gap: 10px; + margin-top: 18px; + padding-top: 16px; + border-top: 1px solid var(--border); + } + + .lp-selection { + min-width: 0; + flex: 1; + color: var(--t2); + font-size: 11px; + line-height: 1.4; + } + + .lp-cancel, + .lpclose { + min-height: 40px; + padding: 9px 20px; + border-radius: 10px; + font-family: inherit; + font-size: 12px; + font-weight: 700; + cursor: pointer; + } + + .lp-cancel { + border: 1px solid var(--border-hi); + background: rgba(255, 255, 255, .035); + color: var(--t2); + } + + .lp-cancel:hover { + color: var(--t1); + background: rgba(255, 255, 255, .07); + } + + .lpclose { + min-width: 170px; + border: 0; + background: var(--grad); + color: white; + box-shadow: 0 8px 22px rgba(var(--workspace-primary-rgb), .25); + } + + .lpclose:hover { + filter: brightness(1.1); + transform: translateY(-1px); + } + + /* Applied workspace themes */ + body[data-workspace-layout] #topbar, + body[data-workspace-layout] #ctb, + body[data-workspace-layout] #chat, + body[data-workspace-layout] #rp, + body[data-workspace-layout] #lsb { + border-color: rgba(var(--workspace-primary-rgb), .13); + } + + body[data-workspace-layout] .layout-btn { + border-color: rgba(var(--workspace-primary-rgb), .28); + background: rgba(var(--workspace-primary-rgb), .08); + } + + body[data-workspace-layout] #rp-resizer:hover::after, + body[data-workspace-layout] #rp-resizer:focus-visible::after, + body[data-workspace-layout] #rp.is-resizing #rp-resizer::after { + background: var(--workspace-primary); + box-shadow: 0 0 10px rgba(var(--workspace-primary-rgb), .55); + } + + @media (min-width: 1025px) { + body.workspace-rail-wide #lsb .sb-btn { + width: 42px; + } + + body.workspace-rail-hidden #lsb, + body.workspace-rail-hidden #pp, + body.workspace-rail-hidden #pp-pull-tab { + display: none !important; + } + + body.workspace-rp-left #rp { + order: 1; + border-left: 0; + border-right: 1px solid rgba(var(--workspace-primary-rgb), .13); + } + + body.workspace-rp-left #canvas { + order: 2; + } + + body.workspace-rp-left #rp-resizer { + right: 0; + left: auto; + } + + body.workspace-rp-left #rp-resizer::after { + right: 0; + left: auto; + } + + body.workspace-chat-side-right #canvas, + body.workspace-chat-side-left #canvas { + display: grid; + grid-template-rows: var(--modebar-h) minmax(0, 1fr); + } + + body.workspace-chat-side-right #canvas { + grid-template-columns: minmax(0, 1fr) minmax(320px, var(--workspace-chat-side-w)); + } + + body.workspace-chat-side-left #canvas { + grid-template-columns: minmax(320px, var(--workspace-chat-side-w)) minmax(0, 1fr); + } + + body.workspace-chat-side-right #ctb, + body.workspace-chat-side-left #ctb { + grid-row: 1; + grid-column: 1 / -1; + } + + body.workspace-chat-side-right #panel-area { + grid-row: 2; + grid-column: 1; + } + + body.workspace-chat-side-left #panel-area { + grid-row: 2; + grid-column: 2; + } + + body.workspace-chat-side-right #chat, + body.workspace-chat-side-left #chat { + grid-row: 2; + width: 100%; + min-width: 0; + height: 100%; + max-height: none !important; + border-top: 0; + padding-bottom: 6px; + } + + body.workspace-chat-side-right #chat { + grid-column: 2; + border-left: 1px solid rgba(var(--workspace-primary-rgb), .13); + } + + body.workspace-chat-side-left #chat { + grid-column: 1; + border-right: 1px solid rgba(var(--workspace-primary-rgb), .13); + } + + body.workspace-chat-side-right #chat-feed, + body.workspace-chat-side-left #chat-feed { + display: flex !important; + flex: 1 1 auto; + max-height: none !important; + } + + body.workspace-chat-side-right #cb, + body.workspace-chat-side-left #cb { + width: 100%; + } + + body.workspace-chat-side-right #sugs, + body.workspace-chat-side-left #sugs { + display: none; + } + + body.workspace-chat-compact { + --transcript-max-h: clamp(90px, 17dvh, 170px); + --composer-h: 44px; + } + + body.workspace-chat-compact #sugs { + display: none; + } + + body.workspace-chat-expanded { + --transcript-max-h: clamp(230px, 38dvh, 480px); + } + + body.workspace-chat-minimal #chat { + max-height: 62px; + } + + body.workspace-chat-minimal #chat-feed, + body.workspace-chat-minimal #ts, + body.workspace-chat-minimal #dv-voice-bar, + body.workspace-chat-minimal #sugs { + display: none !important; + } + + body.workspace-density-compact { + --topbar-h: 40px; + --modebar-h: 32px; + } + + body.workspace-density-compact .pill { + padding: 3px 6px; + } + + body.workspace-status-hidden #sb-wrap { + display: none !important; + } + } + + @media (max-width: 760px) { + .lp-shell { + max-height: calc(100dvh - 24px); + padding: 20px 14px 14px; + } + + .lph { + padding-inline: 32px; + } + + .lgrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + max-height: 66dvh; + } + + .lthumb { + height: 78px; + } + + .lp-actions { + flex-wrap: wrap; + } + + .lp-selection { + flex-basis: 100%; + } + + .lp-cancel, + .lpclose { + flex: 1; + } + } + + /* Auth modal */ + #am-ov { + position: fixed; + inset: 0; + z-index: 600; + background: rgba(2, 4, 16, .96); + backdrop-filter: blur(20px); + display: flex; + align-items: center; + justify-content: center; + } + + .am-box { + width: min(380px, 92vw); + border-radius: 18px; + padding: 32px 28px; + animation: din .22s ease-out; + } + + .am-logo { + text-align: center; + margin-bottom: 20px; + } + + .am-logo-icon { + width: 48px; + height: 48px; + border-radius: 14px; + background: var(--grad); + display: inline-flex; + align-items: center; + justify-content: center; + font-family: var(--fd); + font-weight: 800; + font-size: 22px; + color: white; + box-shadow: 0 0 32px var(--sv-g); + margin-bottom: 10px; + } + + .am-logo h1 { + font-family: var(--fd); + font-size: 22px; + font-weight: 800; + } + + .am-logo p { + font-size: 12px; + color: var(--t3); + margin-top: 4px; + } + + .at-tabs { + display: flex; + gap: 2px; + margin-bottom: 18px; + background: rgba(255, 255, 255, .03); + border-radius: 8px; + padding: 3px; + } + + .at-t { + flex: 1; + text-align: center; + padding: 6px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + color: var(--t3); + cursor: pointer; + transition: all .15s; + } + + .at-t.on { + background: rgba(147, 51, 234, .2); + color: #D8B4FE; + } + + .am-field { + margin-bottom: 12px; + } + + .am-field label { + display: block; + font-size: 10.5px; + font-weight: 600; + color: var(--t3); + margin-bottom: 5px; + } + + .am-field input { + width: 100%; + padding: 9px 12px; + background: rgba(255, 255, 255, .05); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--t1); + font-size: 13px; + font-family: var(--fb); + outline: none; + transition: border-color .15s; + } + + .am-field input:focus { + border-color: rgba(147, 51, 234, .5); + } + + .am-hint { + font-size: 10px; + color: var(--t3); + text-align: center; + margin-bottom: 12px; + padding: 6px; + background: rgba(5, 150, 105, .06); + border: 1px solid rgba(5, 150, 105, .15); + border-radius: 6px; + } + + .am-btn { + width: 100%; + padding: 11px; + border-radius: 10px; + background: var(--grad); + color: white; + font-weight: 700; + font-size: 14px; + border: none; + cursor: pointer; + box-shadow: 0 0 24px rgba(147, 51, 234, .4); + margin-top: 4px; + transition: all .2s; + } + + .am-btn:hover { + opacity: .92; + transform: translateY(-1px); + } + + .am-err { + color: #F87171; + font-size: 11px; + margin-bottom: 8px; + display: none; + text-align: center; + padding: 5px; + background: rgba(239, 68, 68, .08); + border-radius: 6px; + } + + .oab { + width: 100%; + padding: 10px; + border-radius: 9px; + background: rgba(255, 255, 255, .05); + border: 1px solid var(--border); + color: var(--t1); + font-size: 13px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all .15s; + margin-bottom: 8px; + } + + .oab:hover { + background: rgba(255, 255, 255, .09); + } + + .af2 { + font-size: 10px; + color: var(--t3); + text-align: center; + } + + /* ══ INTRO VIDEO OVERLAY ══ */ + #intro-ov { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + width: 100dvw; + height: 100dvh; + z-index: 700; + background: #000; + overflow: hidden; + transition: opacity .5s ease; + } + + #intro-ov.hid { + display: none; + } + + #intro-ov.fade-out { + opacity: 0; + pointer-events: none; + } + + #intro-video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + } + + /* Click-to-start splash — device chooser */ + #intro-splash { + position: absolute; + inset: 0; + z-index: 702; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: radial-gradient(ellipse at center, rgba(10, 15, 30, .88) 0%, rgba(3, 7, 18, .96) 100%); + gap: 28px; + } + + .splash-title { + font-family: 'Syne', sans-serif; + font-weight: 700; + font-size: 22px; + color: #fff; + letter-spacing: .02em; + text-align: center; + } + + .splash-sub { + font-size: 13px; + color: rgba(255, 255, 255, .45); + font-family: 'DM Sans', sans-serif; + margin-top: -16px; + text-align: center; + } + + .device-chooser { + display: flex; + gap: 16px; + flex-wrap: wrap; + justify-content: center; + } + + .dev-choice { + width: 140px; + padding: 24px 12px 18px; + border-radius: 16px; + background: rgba(255, 255, 255, .04); + border: 2px solid rgba(255, 255, 255, .08); + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + cursor: pointer; + transition: all .25s; + color: rgba(255, 255, 255, .6); + font-family: 'DM Sans', sans-serif; + } + + .dev-choice:hover { + background: rgba(139, 92, 246, .12); + border-color: rgba(139, 92, 246, .4); + color: #D8B4FE; + transform: translateY(-2px); + } + + .dev-choice.recommended { + border-color: rgba(139, 92, 246, .5); + background: rgba(139, 92, 246, .1); + color: #D8B4FE; + box-shadow: 0 0 24px rgba(139, 92, 246, .15); + } + + .dev-choice.recommended .dc-badge { + display: block; + } + + .dev-choice i { + font-size: 32px; + transition: all .25s; + } + + .dev-choice:hover i { + transform: scale(1.1); + } + + .dc-label { + font-size: 13px; + font-weight: 600; + } + + .dc-size { + font-size: 10px; + opacity: .5; + font-family: monospace; + } + + .dc-badge { + display: none; + font-size: 9px; + padding: 2px 8px; + border-radius: 8px; + background: rgba(139, 92, 246, .25); + color: #D8B4FE; + font-weight: 600; + letter-spacing: .04em; + margin-top: -4px; + } + + @keyframes introPlayPulse { + + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(139, 92, 246, .3); + } + + 50% { + box-shadow: 0 0 0 16px rgba(139, 92, 246, 0); + } + } + + .dev-choice.recommended { + animation: introPlayPulse 2.5s ease-in-out infinite; + } + + @media (max-width: 600px) { + .splash-title { + font-size: 18px; + } + + .splash-sub { + font-size: 11px; + } + + .device-chooser { + gap: 10px; + } + + .dev-choice { + width: 100px; + padding: 16px 8px 14px; + border-radius: 12px; + } + + .dev-choice i { + font-size: 24px; + } + + .dc-label { + font-size: 11px; + } + + .dc-size { + font-size: 9px; + } + } + + @media (max-width: 380px) { + .dev-choice { + width: 88px; + padding: 12px 6px 10px; + } + + .dev-choice i { + font-size: 20px; + } + + .dc-label { + font-size: 10px; + } + } + + .intro-btn { + position: absolute; + z-index: 701; + background: rgba(255, 255, 255, .12); + backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, .2); + border-radius: 10px; + color: #fff; + font-family: 'DM Sans', sans-serif; + font-size: 13px; + padding: 10px 22px; + cursor: pointer; + transition: all .2s; + display: flex; + align-items: center; + gap: 8px; + } + + .intro-btn:hover { + background: rgba(255, 255, 255, .22); + } + + #intro-vol-control { + position: absolute; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + z-index: 701; + display: flex; + align-items: center; + gap: 12px; + background: rgba(255, 255, 255, .12); + backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, .2); + border-radius: 20px; + padding: 8px 18px; + color: #fff; + font-family: 'DM Sans', sans-serif; + font-size: 12px; + } + + #intro-vol-slider { + width: 100px; + height: 4px; + -webkit-appearance: none; + appearance: none; + background: rgba(255, 255, 255, .2); + border-radius: 2px; + outline: none; + cursor: pointer; + } + + #intro-vol-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: #8B5CF6; + cursor: pointer; + box-shadow: 0 0 8px rgba(139, 92, 246, .6); + } + + #intro-vol-slider::-moz-range-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: #8B5CF6; + cursor: pointer; + box-shadow: 0 0 8px rgba(139, 92, 246, .6); + border: none; + } + + #intro-vol-label { + min-width: 36px; + text-align: right; + font-weight: 600; + color: #D8B4FE; + } + + #intro-skip { + bottom: 40px; + right: 40px; + } + + #intro-skip .skip-timer { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, .3); + border-top-color: #fff; + animation: introSpin 1s linear infinite; + } + + @keyframes introSpin { + to { + transform: rotate(360deg); + } + } + + #intro-mute { + bottom: 40px; + left: 40px; + font-size: 15px; + padding: 10px 14px; + } + + /* ── Responsive intro for tablet / mobile ── */ + @media (max-width: 1024px) { + #intro-skip { + bottom: 24px; + right: 24px; + font-size: 12px; + padding: 8px 16px; + } + + #intro-mute { + bottom: 24px; + left: 24px; + font-size: 13px; + padding: 8px 12px; + } + + #intro-unmute-prompt { + font-size: 14px; + padding: 14px 24px; + gap: 10px; + } + + #intro-unmute-prompt i { + font-size: 18px; + } + } + + @media (max-width: 600px) { + #intro-skip { + bottom: 16px; + right: 16px; + font-size: 11px; + padding: 7px 14px; + border-radius: 8px; + } + + #intro-mute { + bottom: 16px; + left: 16px; + font-size: 12px; + padding: 7px 10px; + border-radius: 8px; + } + + #intro-unmute-prompt { + font-size: 13px; + padding: 12px 20px; + gap: 8px; + border-radius: 12px; + } + + #intro-unmute-prompt i { + font-size: 16px; + } + + #intro-skip .skip-timer { + width: 16px; + height: 16px; + } + } + + #intro-unmute-prompt { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 702; + background: rgba(0, 0, 0, .6); + backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, .25); + border-radius: 16px; + color: #fff; + font-family: 'DM Sans', sans-serif; + font-size: 16px; + padding: 18px 32px; + cursor: pointer; + display: flex; + align-items: center; + gap: 12px; + animation: introUnmutePulse 2s ease-in-out infinite; + transition: opacity .3s; + } + + #intro-unmute-prompt:hover { + background: rgba(139, 92, 246, .3); + } + + #intro-unmute-prompt i { + font-size: 22px; + } + + @keyframes introUnmutePulse { + + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(139, 92, 246, .4); + } + + 50% { + box-shadow: 0 0 0 14px rgba(139, 92, 246, 0); + } + } + + /* ══ SETTINGS MODAL ══ */ + #settings-ov { + position: fixed; + inset: 0; + z-index: 500; + background: rgba(2, 4, 16, .85); + backdrop-filter: blur(16px); + display: flex; + align-items: center; + justify-content: center; + animation: din .18s ease-out; + } + + #settings-ov.hid { + display: none; + } + + .stg-box { + width: min(420px, 92vw); + border-radius: 16px; + padding: 28px 24px; + background: var(--bg-elevated); + border: 1px solid var(--border-hi); + } + + .stg-title { + font-family: 'Syne', sans-serif; + font-size: 16px; + font-weight: 700; + color: var(--t1); + margin-bottom: 20px; + display: flex; + align-items: center; + justify-content: space-between; + } + + .stg-title button { + background: none; + border: none; + color: var(--t3); + cursor: pointer; + font-size: 16px; + padding: 4px; + } + + .stg-title button:hover { + color: var(--t1); + } + + .stg-section { + margin-bottom: 18px; + } + + .stg-label { + font-size: 11px; + color: var(--t2); + text-transform: uppercase; + letter-spacing: .06em; + margin-bottom: 8px; + font-weight: 600; + } + + .stg-desc { + font-size: 10.5px; + color: var(--t3); + margin-bottom: 10px; + line-height: 1.4; + } + + .stg-options { + display: flex; + gap: 8px; + flex-wrap: wrap; + } + + .stg-opt { + padding: 8px 16px; + border-radius: 8px; + font-size: 11.5px; + font-family: 'DM Sans', sans-serif; + cursor: pointer; + border: 1px solid var(--border); + background: rgba(255, 255, 255, .03); + color: var(--t2); + transition: all .15s; + } + + .stg-opt:hover { + border-color: var(--border-hi); + color: var(--t1); + } + + .stg-opt.active { + background: rgba(139, 92, 246, .18); + border-color: rgba(139, 92, 246, .5); + color: #C4B5FD; + font-weight: 600; + } + + .stg-saved { + font-size: 10px; + color: #6EE7B7; + opacity: 0; + transition: opacity .3s; + margin-top: 6px; + } + + .stg-saved.show { + opacity: 1; + } + + /* Toggle switch for settings */ + .stg-toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + } + + .stg-toggle-label { + font-size: 11px; + color: var(--t2); + flex: 1; + } + + .stg-toggle-label small { + display: block; + font-size: 9.5px; + color: var(--t3); + margin-top: 2px; + font-weight: 400; + } + + .stg-toggle { + position: relative; + width: 36px; + height: 20px; + flex-shrink: 0; + } + + .stg-toggle input { + opacity: 0; + width: 0; + height: 0; + position: absolute; + } + + .stg-toggle .stg-slider { + position: absolute; + inset: 0; + background: rgba(255, 255, 255, .1); + border-radius: 10px; + cursor: pointer; + transition: background .2s; + } + + .stg-toggle .stg-slider::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--t3); + transition: transform .2s, background .2s; + } + + .stg-toggle input:checked+.stg-slider { + background: rgba(139, 92, 246, .4); + } + + .stg-toggle input:checked+.stg-slider::after { + transform: translateX(16px); + background: #C4B5FD; + } + + .stg-toggle input:focus-visible+.stg-slider { + outline: 2px solid #8B5CF6; + outline-offset: 2px; + } + + .stg-locked { + opacity: .4; + pointer-events: none; + } + + .stg-locked .stg-slider { + cursor: not-allowed; + } + + .stg-lock-badge { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 8px; + color: var(--t3); + background: rgba(255, 255, 255, .05); + padding: 1px 6px; + border-radius: 4px; + margin-left: 6px; + vertical-align: middle; + } + + /* ══════════════════════════════════════════════════════════ + DEMO / SHOWCASE MODE + Admin sets a project as the default showcase in Action Window + ══════════════════════════════════════════════════════════ */ + #demo-hero { + position: absolute; + inset: 0; + z-index: 8; + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + background: radial-gradient(ellipse at 50% 40%, rgba(15, 20, 40, .88) 0%, rgba(4, 7, 17, .96) 70%); + backdrop-filter: blur(6px); + text-align: center; + padding: 32px 24px; + gap: 12px; + animation: demoFadeIn .5s ease; + } + + #demo-hero.active { + display: flex; + } + + @keyframes demoFadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } + + .demo-cat-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 14px; + border-radius: 20px; + font-size: 10px; + font-weight: 700; + letter-spacing: .08em; + text-transform: uppercase; + } + + .demo-cat-badge.games { + background: rgba(239, 68, 68, .15); + color: #FCA5A5; + border: 1px solid rgba(239, 68, 68, .3); + } + + .demo-cat-badge.web { + background: rgba(59, 130, 246, .15); + color: #93C5FD; + border: 1px solid rgba(59, 130, 246, .3); + } + + .demo-cat-badge.apps { + background: rgba(16, 185, 129, .15); + color: #6EE7B7; + border: 1px solid rgba(16, 185, 129, .3); + } + + .demo-cat-badge.other { + background: rgba(245, 158, 11, .15); + color: #FCD34D; + border: 1px solid rgba(245, 158, 11, .3); + } + + .demo-title { + font-family: var(--fd); + font-size: clamp(26px, 4vw, 48px); + font-weight: 800; + background: linear-gradient(135deg, #F8FAFC 20%, #A78BFA 60%, #60A5FA 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; + line-height: 1.15; + margin: 0; + } + + .demo-sub { + font-size: clamp(12px, 1.3vw, 15px); + color: #64748B; + max-width: 440px; + line-height: 1.6; + } + + .demo-actions { + display: flex; + gap: 16px; + margin-top: 12px; + flex-wrap: wrap; + justify-content: center; + } + + .demo-btn-play { + padding: 14px 44px; + border-radius: 14px; + background: linear-gradient(135deg, #7C3AED 0%, #2563EB 100%); + color: white; + font-weight: 800; + font-size: 16px; + border: none; + cursor: pointer; + box-shadow: 0 0 40px rgba(124, 58, 237, .5), 0 8px 24px rgba(0, 0, 0, .3); + transition: all .25s; + display: flex; + align-items: center; + gap: 10px; + letter-spacing: .02em; + } + + .demo-btn-play:hover { + transform: translateY(-2px) scale(1.03); + box-shadow: 0 0 60px rgba(124, 58, 237, .7), 0 12px 32px rgba(0, 0, 0, .4); + } + + .demo-btn-play:active { + transform: scale(.98); + } + + .demo-btn-play i { + font-size: 18px; + filter: drop-shadow(0 0 6px rgba(255, 255, 255, .4)); + } + + .demo-btn-create { + padding: 14px 36px; + border-radius: 14px; + background: rgba(255, 255, 255, .06); + color: var(--t1); + font-weight: 700; + font-size: 15px; + border: 1px solid rgba(255, 255, 255, .12); + cursor: pointer; + transition: all .2s; + display: flex; + align-items: center; + gap: 8px; + backdrop-filter: blur(4px); + } + + .demo-btn-create:hover { + background: rgba(255, 255, 255, .1); + border-color: rgba(255, 255, 255, .2); + transform: translateY(-1px); + } + + .demo-meta { + font-size: 10px; + color: rgba(255, 255, 255, .2); + margin-top: 8px; + display: flex; + align-items: center; + gap: 8px; + } + + .demo-meta i { + font-size: 8px; + } + + /* Game Carousel */ + #game-carousel { + position: absolute; + bottom: 60px; + left: 50%; + transform: translateX(-50%); + width: 90%; + max-width: 700px; + display: none; + flex-direction: column; + align-items: center; + gap: 12px; + z-index: 9; + } + + #game-carousel.active { + display: flex; + } + + .carousel-container { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + } + + .carousel-arrow { + width: 36px; + height: 36px; + border-radius: 50%; + background: rgba(147, 51, 234, 0.15); + border: 1px solid rgba(147, 51, 234, 0.3); + color: #D8B4FE; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + .carousel-arrow:hover { + background: rgba(147, 51, 234, 0.3); + transform: scale(1.05); + border-color: rgba(147, 51, 234, 0.5); + } + + .carousel-arrow:active { + transform: scale(0.95); + } + + .carousel-track { + flex: 1; + overflow: hidden; + position: relative; + height: 90px; + } + + .carousel-cards { + display: flex; + gap: 12px; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + .game-card { + min-width: 180px; + height: 85px; + background: rgba(20, 24, 45, 0.6); + border: 1px solid rgba(147, 51, 234, 0.2); + border-radius: 6px; + padding: 10px 12px; + cursor: pointer; + transition: all 0.2s; + display: flex; + flex-direction: column; + justify-content: center; + gap: 4px; + } + + .game-card:hover { + border-color: rgba(147, 51, 234, 0.5); + background: rgba(147, 51, 234, 0.1); + transform: translateY(-2px); + } + + .game-card.active { + border-color: #9333EA; + background: rgba(147, 51, 234, 0.2); + box-shadow: 0 0 12px rgba(147, 51, 234, 0.3); + } + + .game-card-title { + font-size: 11px; + font-weight: 600; + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .game-card-sub { + font-size: 9px; + color: var(--t3); + line-height: 1.3; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .carousel-dots { + display: flex; + gap: 6px; + justify-content: center; + } + + .carousel-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + cursor: pointer; + transition: all 0.2s; + } + + .carousel-dot:hover { + background: rgba(255, 255, 255, 0.4); + } + + .carousel-dot.active { + background: #9333EA; + width: 20px; + border-radius: 3px; + } + + /* Admin demo editor in settings */ + .demo-editor { + display: flex; + flex-direction: column; + gap: 8px; + } + + .demo-editor select { + width: 100%; + background: rgba(255, 255, 255, .05); + border: 1px solid var(--border); + color: var(--t1); + padding: 7px 10px; + border-radius: 6px; + font-size: 11px; + font-family: inherit; + } + + .demo-editor select:focus { + outline: none; + border-color: rgba(147, 51, 234, .5); + } + + .demo-editor-row { + display: flex; + align-items: center; + gap: 8px; + } + + .demo-editor-row input { + flex: 1; + background: rgba(255, 255, 255, .05); + border: 1px solid var(--border); + color: var(--t1); + padding: 7px 10px; + border-radius: 6px; + font-size: 11px; + font-family: inherit; + } + + .demo-editor-row input:focus { + outline: none; + border-color: rgba(147, 51, 234, .5); + } + + .demo-editor-apply { + padding: 6px 14px; + background: rgba(147, 51, 234, .25); + border: 1px solid rgba(147, 51, 234, .4); + color: #D8B4FE; + border-radius: 6px; + font-size: 10px; + font-weight: 700; + cursor: pointer; + transition: all .15s; + white-space: nowrap; + } + + .demo-editor-apply:hover { + background: rgba(147, 51, 234, .4); + } + + .demo-editor-clear { + padding: 6px 12px; + background: rgba(239, 68, 68, .15); + border: 1px solid rgba(239, 68, 68, .3); + color: #FCA5A5; + border-radius: 6px; + font-size: 10px; + font-weight: 700; + cursor: pointer; + transition: all .15s; + white-space: nowrap; + } + + .demo-editor-clear:hover { + background: rgba(239, 68, 68, .3); + } + + /* Agentic pulse ring on Play button */ + .demo-btn-play::before { + content: ''; + position: absolute; + inset: -4px; + border-radius: 18px; + background: transparent; + border: 2px solid rgba(124, 58, 237, .3); + animation: demoPulse 2s ease-in-out infinite; + pointer-events: none; + } + + .demo-btn-play { + position: relative; + overflow: visible; + } + + /* ══════════════════════════════════════════════════════════ + GAME MODE — chat + game side by side + Game stays in preview, chat remains fully usable. + Voice narration works seamlessly during gameplay. + ══════════════════════════════════════════════════════════ */ + + /* Floating game control bar — top-right of preview area */ + #game-ctrl-bar { + display: none; + position: absolute; + top: 8px; + right: 8px; + z-index: 12; + background: rgba(10, 13, 24, .85); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, .08); + border-radius: 8px; + padding: 4px 6px; + gap: 4px; + align-items: center; + animation: demoFadeIn .3s ease; + } + + #game-ctrl-bar.active { + display: flex; + } + + #game-ctrl-bar button { + background: rgba(255, 255, 255, .06); + border: 1px solid rgba(255, 255, 255, .08); + color: var(--t2); + font-size: 9px; + padding: 4px 8px; + border-radius: 5px; + cursor: pointer; + transition: all .15s; + white-space: nowrap; + } + + #game-ctrl-bar button:hover { + background: rgba(147, 51, 234, .2); + color: #D8B4FE; + border-color: rgba(147, 51, 234, .3); + } + + #game-ctrl-bar .gcb-label { + font-size: 8px; + color: rgba(255, 255, 255, .25); + text-transform: uppercase; + letter-spacing: .5px; + padding: 0 4px; + } + + /* Mini-chat overlay — floating bubble during immersive game */ + #mini-chat { + display: none; + position: fixed; + bottom: 16px; + right: 16px; + width: 320px; + max-height: 280px; + z-index: 10002; + background: rgba(10, 13, 24, .92); + backdrop-filter: blur(12px); + border: 1px solid rgba(147, 51, 234, .2); + border-radius: 12px; + flex-direction: column; + overflow: hidden; + box-shadow: 0 8px 32px rgba(0, 0, 0, .5); + animation: demoFadeIn .3s ease; + } + + #mini-chat.active { + display: flex; + } + + #mini-chat .mc-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-bottom: 1px solid rgba(255, 255, 255, .06); + } + + #mini-chat .mc-head span { + font-size: 10px; + font-weight: 700; + color: var(--t2); + } + + #mini-chat .mc-head button { + background: none; + border: none; + color: var(--t3); + cursor: pointer; + font-size: 10px; + } + + #mini-chat .mc-messages { + flex: 1; + overflow-y: auto; + padding: 8px 12px; + font-size: 11px; + color: var(--t2); + max-height: 180px; + } + + #mini-chat .mc-messages .mc-msg { + margin-bottom: 6px; + padding: 4px 8px; + border-radius: 6px; + background: rgba(255, 255, 255, .04); + line-height: 1.4; + } + + #mini-chat .mc-messages .mc-msg.mc-user { + background: rgba(147, 51, 234, .15); + text-align: right; + } + + #mini-chat .mc-input { + display: flex; + border-top: 1px solid rgba(255, 255, 255, .06); + padding: 6px 8px; + gap: 6px; + } + + #mini-chat .mc-input input { + flex: 1; + background: rgba(255, 255, 255, .05); + border: 1px solid rgba(255, 255, 255, .08); + color: var(--t1); + padding: 5px 8px; + border-radius: 6px; + font-size: 10px; + outline: none; + } + + #mini-chat .mc-input input:focus { + border-color: rgba(147, 51, 234, .4); + } + + #mini-chat .mc-input button { + background: rgba(147, 51, 234, .3); + border: none; + color: #D8B4FE; + padding: 5px 10px; + border-radius: 6px; + cursor: pointer; + font-size: 10px; + } + + /* Game voice indicator — pulsing mic icon during TTS in game mode */ + #game-voice-indicator { + display: none; + position: absolute; + bottom: 8px; + left: 8px; + z-index: 12; + background: rgba(10, 13, 24, .8); + backdrop-filter: blur(6px); + border: 1px solid rgba(16, 185, 129, .3); + border-radius: 20px; + padding: 4px 10px; + font-size: 9px; + color: #6EE7B7; + gap: 5px; + align-items: center; + animation: demoFadeIn .3s ease; + } + + #game-voice-indicator.active { + display: flex; + } + + #game-voice-indicator i { + animation: demoPulse 1.5s ease-in-out infinite; + } + + @keyframes demoPulse { + + 0%, + 100% { + transform: scale(1); + opacity: .6; + } + + 50% { + transform: scale(1.06); + opacity: 0; + } + } + + /* ══════════════════════════════════════════════════════════ + VOICE STUDIO — Professional TTS Control Panel + Full voice tuning, presets, waveform, agent narration + ══════════════════════════════════════════════════════════ */ + #voice-studio-overlay { + display: none; + position: fixed; + inset: 0; + z-index: 9999; + background: rgba(0, 0, 0, .7); + backdrop-filter: blur(8px); + align-items: center; + justify-content: center; + } + + #voice-studio-overlay.open { + display: flex; + } + + #voice-studio { + width: 720px; + max-width: 96vw; + max-height: 88vh; + background: linear-gradient(135deg, rgba(15, 17, 28, .98), rgba(20, 22, 38, .98)); + border: 1px solid rgba(147, 51, 234, .25); + border-radius: 16px; + box-shadow: 0 24px 80px rgba(0, 0, 0, .6), 0 0 40px rgba(147, 51, 234, .1); + overflow: hidden; + display: flex; + flex-direction: column; + } + + .vs-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px 12px; + border-bottom: 1px solid rgba(255, 255, 255, .06); + } + + .vs-header h2 { + margin: 0; + font-size: 16px; + font-weight: 800; + color: var(--t1); + display: flex; + align-items: center; + gap: 8px; + } + + .vs-header h2 i { + color: #A78BFA; + font-size: 14px; + } + + .vs-header .vs-close { + width: 30px; + height: 30px; + border-radius: 8px; + background: rgba(255, 255, 255, .06); + border: none; + color: var(--t3); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all .15s; + } + + .vs-header .vs-close:hover { + background: rgba(239, 68, 68, .2); + color: #FCA5A5; + } + + /* Voice Studio Tabs */ + .vs-tabs { + display: flex; + gap: 0; + padding: 0 20px; + border-bottom: 1px solid rgba(255, 255, 255, .06); + } + + .vs-tab { + padding: 10px 16px; + font-size: 10.5px; + font-weight: 700; + color: var(--t3); + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all .15s; + display: flex; + align-items: center; + gap: 5px; + text-transform: uppercase; + letter-spacing: .5px; + } + + .vs-tab:hover { + color: var(--t2); + } + + .vs-tab.on { + color: #D8B4FE; + border-bottom-color: #7C3AED; + } + + .vs-tab i { + font-size: 10px; + } + + /* Voice Studio Body */ + .vs-body { + flex: 1; + overflow-y: auto; + padding: 16px 20px; + } + + .vs-pane { + display: none; + } + + .vs-pane.on { + display: block; + } + + /* Voice Cards */ + .vs-voice-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 16px; + } + + .vs-voice-card { + padding: 12px 14px; + border-radius: 10px; + cursor: pointer; + background: rgba(255, 255, 255, .03); + border: 1px solid rgba(255, 255, 255, .06); + transition: all .2s; + position: relative; + } + + .vs-voice-card:hover { + background: rgba(255, 255, 255, .06); + border-color: rgba(255, 255, 255, .12); + } + + .vs-voice-card.selected { + background: rgba(147, 51, 234, .12); + border-color: rgba(147, 51, 234, .4); + box-shadow: 0 0 16px rgba(147, 51, 234, .15); + } + + .vs-voice-card .vc-name { + font-size: 12px; + font-weight: 700; + color: var(--t1); + margin-bottom: 2px; + } + + .vs-voice-card .vc-desc { + font-size: 9.5px; + color: var(--t3); + line-height: 1.3; + } + + .vs-voice-card .vc-grade { + position: absolute; + top: 8px; + right: 10px; + font-size: 8px; + font-weight: 800; + padding: 2px 6px; + border-radius: 4px; + } + + .vc-grade.a { + background: rgba(34, 197, 94, .2); + color: #6EE7B7; + } + + .vc-grade.b { + background: rgba(59, 130, 246, .2); + color: #93C5FD; + } + + .vc-grade.c { + background: rgba(251, 191, 36, .2); + color: #FCD34D; + } + + .vc-grade.d { + background: rgba(156, 163, 175, .2); + color: #9CA3AF; + } + + .vs-voice-card .vc-preview-btn { + margin-top: 6px; + padding: 3px 10px; + border-radius: 5px; + font-size: 9px; + font-weight: 700; + background: rgba(147, 51, 234, .2); + border: 1px solid rgba(147, 51, 234, .3); + color: #D8B4FE; + cursor: pointer; + transition: all .15s; + } + + .vs-voice-card .vc-preview-btn:hover { + background: rgba(147, 51, 234, .35); + } + + .vs-voice-card .vc-preview-btn.playing { + background: rgba(239, 68, 68, .2); + border-color: rgba(239, 68, 68, .3); + color: #FCA5A5; + } + + /* Gender/Accent Tabs inside voice pane */ + .vs-filter-bar { + display: flex; + gap: 6px; + margin-bottom: 14px; + flex-wrap: wrap; + } + + .vs-filter-btn { + padding: 5px 12px; + border-radius: 6px; + font-size: 9.5px; + font-weight: 700; + background: rgba(255, 255, 255, .04); + border: 1px solid rgba(255, 255, 255, .08); + color: var(--t3); + cursor: pointer; + transition: all .15s; + } + + .vs-filter-btn:hover { + background: rgba(255, 255, 255, .08); + } + + .vs-filter-btn.on { + background: rgba(147, 51, 234, .2); + border-color: rgba(147, 51, 234, .4); + color: #D8B4FE; + } + + /* Sliders */ + .vs-slider-group { + margin-bottom: 14px; + } + + .vs-slider-label { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 10px; + color: var(--t3); + margin-bottom: 5px; + } + + .vs-slider-label .vs-val { + color: #D8B4FE; + font-weight: 700; + font-family: 'JetBrains Mono', monospace; + } + + .vs-slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 6px; + border-radius: 3px; + background: rgba(255, 255, 255, .08); + outline: none; + transition: background .15s; + } + + .vs-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: #7C3AED; + border: 2px solid #D8B4FE; + cursor: pointer; + box-shadow: 0 0 8px rgba(124, 58, 237, .4); + transition: all .15s; + } + + .vs-slider::-webkit-slider-thumb:hover { + transform: scale(1.15); + box-shadow: 0 0 14px rgba(124, 58, 237, .6); + } + + .vs-slider::-moz-range-thumb { + width: 16px; + height: 16px; + border-radius: 50%; + background: #7C3AED; + border: 2px solid #D8B4FE; + cursor: pointer; + } + + /* Waveform Visualizer */ + .vs-waveform { + width: 100%; + height: 60px; + border-radius: 8px; + background: rgba(0, 0, 0, .3); + border: 1px solid rgba(255, 255, 255, .06); + margin-bottom: 14px; + position: relative; + overflow: hidden; + } + + .vs-waveform canvas { + width: 100%; + height: 100%; + } + + .vs-waveform .wf-status { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + color: var(--t3); + pointer-events: none; + } + + /* Voice Blend */ + .vs-blend-row { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; + padding: 10px 12px; + border-radius: 8px; + background: rgba(255, 255, 255, .03); + border: 1px solid rgba(255, 255, 255, .06); + } + + .vs-blend-row select { + flex: 1; + background: rgba(255, 255, 255, .05); + border: 1px solid var(--border); + color: var(--t1); + padding: 6px 8px; + border-radius: 5px; + font-size: 10px; + font-family: inherit; + } + + .vs-blend-row select:focus { + outline: none; + border-color: rgba(147, 51, 234, .5); + } + + .vs-blend-weight { + width: 50px; + text-align: center; + background: rgba(255, 255, 255, .05); + border: 1px solid var(--border); + color: #D8B4FE; + padding: 5px; + border-radius: 5px; + font-size: 10px; + font-weight: 700; + font-family: inherit; + } + + /* EQ Bars */ + .vs-eq-row { + display: flex; + gap: 14px; + align-items: flex-end; + justify-content: center; + padding: 12px 0; + margin-bottom: 14px; + } + + .vs-eq-band { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + } + + .vs-eq-band input[type="range"] { + -webkit-appearance: none; + appearance: none; + writing-mode: vertical-lr; + direction: rtl; + width: 20px; + height: 80px; + accent-color: var(--mode-active); + } + + .vs-eq-band label { + font-size: 8px; + color: var(--t3); + font-weight: 700; + text-transform: uppercase; + } + + /* Preset Cards */ + .vs-preset-grid { + display: flex; + flex-direction: column; + gap: 8px; + } + + .vs-preset-card { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: 8px; + background: rgba(255, 255, 255, .03); + border: 1px solid rgba(255, 255, 255, .06); + transition: all .15s; + cursor: pointer; + } + + .vs-preset-card:hover { + background: rgba(255, 255, 255, .06); + } + + .vs-preset-card.active { + border-color: rgba(147, 51, 234, .4); + background: rgba(147, 51, 234, .08); + } + + .vs-preset-card .vp-icon { + width: 32px; + height: 32px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + } + + .vs-preset-card .vp-icon.male { + background: rgba(59, 130, 246, .15); + color: #93C5FD; + } + + .vs-preset-card .vp-icon.female { + background: rgba(236, 72, 153, .15); + color: #F9A8D4; + } + + .vs-preset-card .vp-info { + flex: 1; + } + + .vs-preset-card .vp-name { + font-size: 11px; + font-weight: 700; + color: var(--t1); + } + + .vs-preset-card .vp-meta { + font-size: 9px; + color: var(--t3); + } + + .vs-preset-card .vp-actions { + display: flex; + gap: 4px; + } + + .vs-preset-card .vp-actions button { + width: 26px; + height: 26px; + border-radius: 5px; + background: rgba(255, 255, 255, .05); + border: none; + color: var(--t3); + cursor: pointer; + font-size: 9px; + transition: all .15s; + } + + .vs-preset-card .vp-actions button:hover { + background: rgba(255, 255, 255, .12); + color: var(--t1); + } + + /* Test Area */ + .vs-test-area { + display: flex; + gap: 8px; + margin-bottom: 14px; + } + + .vs-test-input { + flex: 1; + background: rgba(255, 255, 255, .05); + border: 1px solid var(--border); + color: var(--t1); + padding: 8px 12px; + border-radius: 8px; + font-size: 11px; + font-family: inherit; + resize: none; + min-height: 36px; + } + + .vs-test-input:focus { + outline: none; + border-color: rgba(147, 51, 234, .5); + } + + .vs-test-btn { + padding: 8px 16px; + border-radius: 8px; + font-size: 11px; + font-weight: 700; + background: linear-gradient(135deg, #7C3AED, #9333EA); + border: none; + color: #fff; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + transition: all .15s; + white-space: nowrap; + } + + .vs-test-btn:hover { + filter: brightness(1.15); + transform: translateY(-1px); + } + + .vs-test-btn:disabled { + opacity: .5; + cursor: not-allowed; + transform: none; + filter: none; + } + + .vs-test-btn.playing { + background: linear-gradient(135deg, #DC2626, #EF4444); + } + + /* Agent Voice Config */ + .vs-agent-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 8px; + background: rgba(255, 255, 255, .03); + border: 1px solid rgba(255, 255, 255, .06); + margin-bottom: 8px; + } + + .vs-agent-row .va-dot { + width: 8px; + height: 8px; + border-radius: 50%; + } + + .vs-agent-row .va-dot.sv { + background: #D8B4FE; + } + + .vs-agent-row .va-dot.cd { + background: #93C5FD; + } + + .vs-agent-row .va-dot.qa { + background: #6EE7B7; + } + + .vs-agent-row .va-dot.as { + background: #FCD34D; + } + + .vs-agent-row .va-name { + font-size: 11px; + font-weight: 700; + color: var(--t1); + flex: 1; + } + + .vs-agent-row select { + background: rgba(255, 255, 255, .05); + border: 1px solid var(--border); + color: var(--t2); + padding: 4px 8px; + border-radius: 5px; + font-size: 9.5px; + font-family: inherit; + } + + .vs-agent-toggle { + display: flex; + align-items: center; + gap: 6px; + } + + /* Section Titles */ + .vs-section-title { + font-size: 10px; + font-weight: 800; + color: var(--t3); + text-transform: uppercase; + letter-spacing: .8px; + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 6px; + } + + .vs-section-title i { + font-size: 9px; + color: #A78BFA; + } + + .vs-divider { + height: 1px; + background: rgba(255, 255, 255, .06); + margin: 16px 0; + } + + /* Footer Actions */ + .vs-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 20px; + border-top: 1px solid rgba(255, 255, 255, .06); + } + + .vs-footer-status { + font-size: 9px; + color: var(--t3); + display: flex; + align-items: center; + gap: 6px; + } + + .vs-footer-status .vs-dot { + width: 6px; + height: 6px; + border-radius: 50%; + } + + .vs-footer-status .vs-dot.online { + background: #22C55E; + box-shadow: 0 0 6px rgba(34, 197, 94, .5); + } + + .vs-footer-status .vs-dot.warn { + background: #F59E0B; + box-shadow: 0 0 6px rgba(245, 158, 11, .5); + } + + .vs-footer-status .vs-dot.offline { + background: #EF4444; + } + + .vs-footer-actions { + display: flex; + gap: 6px; + } + + .vs-footer-actions button { + padding: 6px 14px; + border-radius: 6px; + font-size: 10px; + font-weight: 700; + cursor: pointer; + transition: all .15s; + } + + .vs-btn-save { + background: rgba(147, 51, 234, .25); + border: 1px solid rgba(147, 51, 234, .4); + color: #D8B4FE; + } + + .vs-btn-save:hover { + background: rgba(147, 51, 234, .4); + } + + .vs-btn-export { + background: rgba(255, 255, 255, .05); + border: 1px solid rgba(255, 255, 255, .08); + color: var(--t3); + } + + .vs-btn-export:hover { + background: rgba(255, 255, 255, .1); + color: var(--t1); + } + + /* ── Inline Voice Control Bar (above chat input) ── */ + #dv-voice-bar { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + margin: 0 8px 2px; + width: min(calc(100% - 16px), 1120px); + max-width: 1120px; + box-sizing: border-box; + background: rgba(147, 51, 234, .08); + border: 1px solid rgba(147, 51, 234, .15); + border-radius: 8px; + font-size: 10px; + color: var(--t3); + transition: all .2s; + } + + #dv-voice-bar:hover { + border-color: rgba(147, 51, 234, .24); + } + + #dv-voice-bar .dvb-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + } + + #dv-voice-bar .dvb-dot.on { + background: #4ade80; + box-shadow: 0 0 5px rgba(74, 222, 128, .5); + } + + #dv-voice-bar .dvb-dot.warn { + background: #fbbf24; + } + + #dv-voice-bar .dvb-dot.off { + background: #ef4444; + } + + #dv-voice-bar .dvb-mode, + #dv-voice-bar .dvb-engine { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 7px; + border-radius: 5px; + border: 1px solid rgba(255, 255, 255, .1); + background: rgba(255, 255, 255, .045); + font-weight: 600; + font-size: 10px; + font-family: inherit; + line-height: 1.4; + cursor: pointer; + transition: all .15s; + } + + #dv-voice-bar .dvb-mode { + color: var(--mode-active, #D8B4FE); + background: var(--mode-active-tint, rgba(147, 51, 234, .12)); + border-color: color-mix(in srgb, var(--mode-active, #D8B4FE) 34%, transparent); + } + + #dv-voice-bar .dvb-mode:hover { + background: var(--mode-active-tint, rgba(147, 51, 234, .2)); + color: var(--t1); + } + + #dv-voice-bar .dvb-engine { + color: #FCD34D; + background: rgba(245, 158, 11, .1); + border-color: rgba(245, 158, 11, .22); + } + + #dv-voice-bar .dvb-engine:hover { + background: rgba(245, 158, 11, .18); + color: #FDE68A; + } + + #dv-voice-bar .dvb-voice { + color: #D8B4FE; + font-weight: 600; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; + background: rgba(147, 51, 234, .12); + border: 1px solid rgba(147, 51, 234, .22); + } + + #dv-voice-bar .dvb-voice:hover { + background: rgba(147, 51, 234, .25); + } + + #dv-voice-bar .dvb-btn { + padding: 2px 6px; + border-radius: 4px; + cursor: pointer; + border: none; + background: rgba(255, 255, 255, .06); + color: var(--t3); + font-size: 10px; + font-family: inherit; + transition: all .15s; + } + + #dv-voice-bar .dvb-btn:hover { + background: rgba(255, 255, 255, .12); + color: var(--t1); + } + + #dv-voice-bar .dvb-btn.active { + background: rgba(147, 51, 234, .2); + color: #D8B4FE; + } + + #dv-voice-bar .dvb-sep { + width: 1px; + height: 14px; + background: rgba(255, 255, 255, .08); + } + + .dvb-speaking { + animation: dvbPulse 1s infinite; + background: rgba(147, 51, 234, .18) !important; + box-shadow: 0 0 12px rgba(147, 51, 234, .15); + } + + @keyframes dvbPulse { + + 0%, + 100% { + opacity: 1 + } + + 50% { + opacity: .7 + } + } + + /* ── Speaking Overlay: waveform + mouth animation ── */ + #dv-speak-overlay { + display: none; + position: absolute; + bottom: 100%; + left: 0; + right: 0; + height: 64px; + margin-bottom: 2px; + background: rgba(8, 6, 20, .95); + border: 1px solid rgba(147, 51, 234, .25); + border-radius: 12px; + overflow: hidden; + z-index: 200; + backdrop-filter: blur(12px); + align-items: center; + gap: 0; + box-shadow: 0 -4px 24px rgba(0, 0, 0, .4); + transition: opacity .15s, transform .15s; + opacity: 0; + transform: translateY(6px); + } + + #dv-speak-overlay.active { + display: flex; + opacity: 1; + transform: translateY(0); + } + + /* Mouth / Avatar indicator */ + .dv-mouth-wrap { + width: 48px; + height: 64px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + position: relative; + } + + .dv-mouth-circle { + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + position: relative; + transition: box-shadow .08s; + } + + .dv-mouth-circle.female { + background: rgba(244, 114, 182, .15); + border: 1.5px solid rgba(244, 114, 182, .4); + color: #F472B6; + box-shadow: 0 0 8px rgba(244, 114, 182, .2); + } + + .dv-mouth-circle.male { + background: rgba(96, 165, 250, .15); + border: 1.5px solid rgba(96, 165, 250, .4); + color: #60A5FA; + box-shadow: 0 0 8px rgba(96, 165, 250, .2); + } + + /* Mouth open/close animation driven by JS */ + .dv-mouth-bar { + position: absolute; + bottom: 8px; + left: 50%; + transform: translateX(-50%); + width: 12px; + height: 3px; + border-radius: 2px; + transition: height .06s, width .06s, border-radius .06s; + } + + .dv-mouth-bar.female { + background: #F472B6; + } + + .dv-mouth-bar.male { + background: #60A5FA; + } + + /* Waveform canvas area */ + .dv-wf-area { + flex: 1; + height: 100%; + position: relative; + } + + .dv-wf-area canvas { + width: 100%; + height: 100%; + display: block; + } + + /* Voice name + engine label */ + .dv-speak-info { + width: 80px; + flex-shrink: 0; + padding: 0 8px; + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: center; + gap: 2px; + } + + .dv-speak-name { + font-size: 10px; + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 72px; + } + + .dv-speak-name.female { + color: #F472B6; + } + + .dv-speak-name.male { + color: #60A5FA; + } + + .dv-speak-engine { + font-size: 8px; + color: var(--t3); + text-transform: uppercase; + letter-spacing: .5px; + } + + /* Glow ring animation around mouth when speaking */ + @keyframes dvMouthGlow { + 0% { + box-shadow: 0 0 6px rgba(244, 114, 182, .2); + } + + 50% { + box-shadow: 0 0 18px rgba(244, 114, 182, .5); + } + + 100% { + box-shadow: 0 0 6px rgba(244, 114, 182, .2); + } + } + + @keyframes dvMouthGlowMale { + 0% { + box-shadow: 0 0 6px rgba(96, 165, 250, .2); + } + + 50% { + box-shadow: 0 0 18px rgba(96, 165, 250, .5); + } + + 100% { + box-shadow: 0 0 6px rgba(96, 165, 250, .2); + } + } + + .dv-mouth-circle.female.speaking { + animation: dvMouthGlow .4s ease-in-out infinite; + } + + .dv-mouth-circle.male.speaking { + animation: dvMouthGlowMale .4s ease-in-out infinite; + } + + /* ══════════════════════════════════════════════════════════ + WAVEFORM COLOR SYSTEM — 16 neon colors + custom picker + ══════════════════════════════════════════════════════════ */ + .wf-color-grid { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 4px; + margin: 8px 0; + } + + .wf-color-swatch { + width: 100%; + aspect-ratio: 1; + border-radius: 6px; + cursor: pointer; + border: 2px solid transparent; + transition: all .15s; + position: relative; + overflow: hidden; + } + + .wf-color-swatch:hover { + transform: scale(1.15); + z-index: 2; + box-shadow: 0 0 12px currentColor; + } + + .wf-color-swatch.selected { + border-color: #fff; + box-shadow: 0 0 16px currentColor; + transform: scale(1.1); + } + + .wf-color-swatch::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 4px; + background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, .3), transparent 60%); + } + + /* ══════════════════════════════════════════════════════════ + WAVEFORM TYPE SELECTOR — 12 SOTA visualizations + ══════════════════════════════════════════════════════════ */ + .wf-type-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 6px; + margin: 8px 0; + } + + .wf-type-card { + padding: 8px 4px; + border-radius: 8px; + text-align: center; + cursor: pointer; + background: rgba(255, 255, 255, .03); + border: 1px solid rgba(255, 255, 255, .06); + transition: all .2s; + font-size: 9px; + color: var(--t3); + } + + .wf-type-card:hover { + background: rgba(147, 51, 234, .12); + border-color: rgba(147, 51, 234, .3); + color: var(--t1); + transform: translateY(-1px); + } + + .wf-type-card.selected { + background: rgba(147, 51, 234, .2); + border-color: rgba(147, 51, 234, .5); + color: #D8B4FE; + box-shadow: 0 2px 12px rgba(147, 51, 234, .2); + } + + .wf-type-card i { + display: block; + font-size: 16px; + margin-bottom: 3px; + } + + /* ══════════════════════════════════════════════════════════ + VISUAL EFFECTS SYSTEM — Hover, particles, flames, glow + ══════════════════════════════════════════════════════════ */ + + /* Universal hover lift for interactive elements */ + .dv-fx-hover { + transition: transform .18s cubic-bezier(.34, 1.56, .64, 1), box-shadow .18s, filter .18s !important; + } + + .dv-fx-hover:hover { + transform: translateY(-2px) scale(1.02) !important; + box-shadow: 0 6px 20px rgba(0, 0, 0, .3), 0 0 15px rgba(147, 51, 234, .15) !important; + } + + .dv-fx-hover:active { + transform: translateY(0) scale(.98) !important; + transition-duration: .06s !important; + } + + /* Button ripple effect */ + .dv-ripple { + position: relative; + overflow: hidden; + } + + .dv-ripple::after { + content: ''; + position: absolute; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, .2); + transform: translate(-50%, -50%); + pointer-events: none; + transition: width .4s, height .4s, opacity .4s; + opacity: 0; + } + + .dv-ripple:active::after { + width: 300px; + height: 300px; + opacity: 0; + transition: 0s; + } + + /* Card shimmer on hover */ + .dv-shimmer { + position: relative; + overflow: hidden; + } + + .dv-shimmer::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 60%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .04), transparent); + transition: left .6s; + pointer-events: none; + z-index: 1; + } + + .dv-shimmer:hover::before { + left: 150%; + } + + /* Glow border pulse */ + .dv-glow-border { + position: relative; + } + + .dv-glow-border::after { + content: ''; + position: absolute; + inset: -1px; + border-radius: inherit; + background: linear-gradient(135deg, rgba(147, 51, 234, .3), rgba(96, 165, 250, .3), rgba(244, 114, 182, .3)); + background-size: 300% 300%; + z-index: -1; + opacity: 0; + transition: opacity .3s; + animation: dvGlowShift 3s ease infinite; + } + + .dv-glow-border:hover::after { + opacity: 1; + } + + @keyframes dvGlowShift { + 0% { + background-position: 0% 50% + } + + 50% { + background-position: 100% 50% + } + + 100% { + background-position: 0% 50% + } + } + + /* Particle burst on click */ + .dv-particles { + position: absolute; + pointer-events: none; + z-index: 9999; + } + + .dv-particle { + position: absolute; + width: 4px; + height: 4px; + border-radius: 50%; + animation: dvParticleFly .6s ease-out forwards; + } + + @keyframes dvParticleFly { + 0% { + transform: translate(0, 0) scale(1); + opacity: 1; + } + + 100% { + transform: translate(var(--dx), var(--dy)) scale(0); + opacity: 0; + } + } + + /* Flame effect (toggle-able) */ + .dv-flame-wrap { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 100%; + height: 30px; + pointer-events: none; + overflow: hidden; + opacity: 0; + transition: opacity .3s; + } + + .dv-flame-active .dv-flame-wrap { + opacity: 1; + } + + .dv-flame { + position: absolute; + bottom: 0; + border-radius: 50% 50% 20% 20%; + filter: blur(2px); + animation: dvFlameFlicker .3s ease-in-out infinite alternate; + } + + @keyframes dvFlameFlicker { + 0% { + transform: scaleY(1) translateY(0); + opacity: .8 + } + + 100% { + transform: scaleY(1.3) translateY(-4px); + opacity: 1 + } + } + + /* Static / glitch effect (toggle-able) */ + .dv-static-overlay { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 10; + background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255, 255, 255, .015) 2px, rgba(255, 255, 255, .015) 4px); + opacity: 0; + transition: opacity .2s; + } + + .dv-static-active:hover .dv-static-overlay { + opacity: 1; + } + + /* Neon text glow on hover */ + .dv-neon-text:hover { + text-shadow: 0 0 7px currentColor, 0 0 10px currentColor, 0 0 21px currentColor; + } + + /* Smooth panel transitions */ + .dv-slide-up { + animation: dvSlideUp .3s cubic-bezier(.16, 1, .3, 1); + } + + @keyframes dvSlideUp { + from { + transform: translateY(20px); + opacity: 0 + } + + to { + transform: translateY(0); + opacity: 1 + } + } + + .dv-slide-in { + animation: dvSlideIn .3s cubic-bezier(.16, 1, .3, 1); + } + + @keyframes dvSlideIn { + from { + transform: translateX(-20px); + opacity: 0 + } + + to { + transform: translateX(0); + opacity: 1 + } + } + + .dv-fade-in { + animation: dvFadeIn .25s ease-out; + } + + @keyframes dvFadeIn { + from { + opacity: 0 + } + + to { + opacity: 1 + } + } + + .dv-scale-in { + animation: dvScaleIn .2s cubic-bezier(.16, 1, .3, 1); + } + + @keyframes dvScaleIn { + from { + transform: scale(.9); + opacity: 0 + } + + to { + transform: scale(1); + opacity: 1 + } + } + + /* Electric border spark effect */ + .dv-spark { + position: relative; + } + + .dv-spark::before { + content: ''; + position: absolute; + inset: -2px; + border-radius: inherit; + background: conic-gradient(from var(--spark-angle, 0deg), transparent 80%, rgba(147, 51, 234, .6)); + z-index: -1; + opacity: 0; + transition: opacity .3s; + } + + .dv-spark:hover::before { + opacity: 1; + animation: dvSparkRotate 1.5s linear infinite; + } + + @keyframes dvSparkRotate { + to { + --spark-angle: 360deg + } + } + + /* ══════════════════════════════════════════════════════════ + ICON/THEME STUDIO — Settings panel styles + ══════════════════════════════════════════════════════════ */ + .icon-studio-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + margin: 8px 0; + } + + .icon-pack-card { + padding: 10px; + border-radius: 8px; + text-align: center; + cursor: pointer; + background: rgba(255, 255, 255, .03); + border: 1px solid rgba(255, 255, 255, .06); + transition: all .2s; + position: relative; + } + + .icon-pack-card:hover { + background: rgba(147, 51, 234, .1); + border-color: rgba(147, 51, 234, .3); + transform: translateY(-2px); + } + + .icon-pack-card.active { + background: rgba(147, 51, 234, .15); + border-color: rgba(147, 51, 234, .5); + box-shadow: 0 2px 12px rgba(147, 51, 234, .2); + } + + .icon-pack-card.broken { + opacity: .5; + border-color: rgba(239, 68, 68, .3); + } + + .icon-pack-card.broken::after { + content: 'BROKEN'; + position: absolute; + top: 4px; + right: 4px; + font-size: 7px; + font-weight: 700; + color: #FCA5A5; + background: rgba(239, 68, 68, .2); + padding: 1px 4px; + border-radius: 3px; + } + + .icon-pack-name { + font-size: 10px; + font-weight: 700; + color: var(--t1); + margin-top: 4px; + } + + .icon-pack-count { + font-size: 8px; + color: var(--t3); + } + + .icon-pack-preview { + font-size: 18px; + display: flex; + gap: 4px; + justify-content: center; + margin-bottom: 4px; + } + + .theme-backup-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + margin: 8px 0; + background: rgba(255, 255, 255, .02); + border-radius: 6px; + border: 1px solid var(--border); + } + + .theme-backup-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + + .theme-backup-dot.ok { + background: #4ade80; + box-shadow: 0 0 6px rgba(74, 222, 128, .3); + } + + .theme-backup-dot.warn { + background: #fbbf24; + } + + .theme-backup-dot.err { + background: #f87171; + } + + /* ── Quick Voice Picker Popup ── */ + #dv-voice-picker { + display: none; + position: absolute; + bottom: 100%; + left: 0; + right: 0; + max-height: 220px; + overflow-y: auto; + margin-bottom: 4px; + background: rgba(15, 10, 30, .97); + border: 1px solid rgba(147, 51, 234, .2); + border-radius: 10px; + padding: 6px; + z-index: 999; + box-shadow: 0 -8px 30px rgba(0, 0, 0, .5); + } + + #dv-voice-picker.open { + display: block; + } + + .dvp-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + border-radius: 6px; + cursor: pointer; + font-size: 10px; + color: var(--t2); + transition: all .12s; + } + + .dvp-item:hover { + background: rgba(147, 51, 234, .15); + } + + .dvp-item.selected { + background: rgba(147, 51, 234, .2); + border: 1px solid rgba(147, 51, 234, .3); + } + + .dvp-item .dvp-name { + font-weight: 600; + } + + .dvp-item .dvp-meta { + color: var(--t3); + font-size: 9px; + } + + .dvp-item .dvp-grade { + font-size: 8px; + font-weight: 700; + padding: 1px 5px; + border-radius: 3px; + margin-left: auto; + } + + @media (max-width:640px) { + #voice-studio { + width: 98vw; + max-height: 95vh; + border-radius: 12px; + } + + .vs-voice-grid { + grid-template-columns: 1fr; + } + + .vs-tabs { + overflow-x: auto; + } + } + + /* ══════════════════════════════════════════════════════════ + IMMERSIVE PROJECT VIEW + Maximizes preview space; edge-hover reveals hidden UI + ══════════════════════════════════════════════════════════ */ + .immersive #topbar, + .immersive #sb-wrap, + .immersive #lsb, + .immersive #pp, + .immersive #rp, + .immersive #chat, + .immersive #ctb, + .immersive .btb, + .immersive .frame-chrome, + .immersive #bst { + opacity: 0; + pointer-events: none; + position: absolute; + z-index: 200; + transition: opacity .25s ease, transform .25s ease; + } + + .immersive #topbar { + top: 0; + left: 0; + right: 0; + transform: translateY(-100%); + height: 42px; + } + + .immersive #sb-wrap { + top: 0; + left: 0; + right: 0; + transform: translateY(-100%); + } + + .immersive #lsb { + top: 0; + left: 0; + bottom: 0; + transform: translateX(-100%); + width: 48px; + } + + .immersive #pp { + transform: translateX(-100%); + } + + .immersive #rp { + top: 0; + right: 0; + bottom: 0; + transform: translateX(100%); + width: var(--right-panel-w); + border-left: 1px solid var(--border); + } + + .immersive #chat { + bottom: 0; + left: 0; + right: 0; + transform: translateY(100%); + } + + /* When edge-hover reveals panels */ + .immersive.edge-top #topbar { + opacity: 1; + pointer-events: auto; + transform: translateY(0); + position: fixed; + width: 100%; + } + + .immersive.edge-left #lsb { + opacity: 1; + pointer-events: auto; + transform: translateX(0); + position: fixed; + height: 100vh; + background: var(--bg); + } + + .immersive.edge-right #rp { + opacity: 1; + pointer-events: auto; + transform: translateX(0); + position: fixed; + height: 100vh; + background: rgba(4, 8, 20, 0.95); + } + + .immersive.edge-bottom #chat { + opacity: 1; + pointer-events: auto; + transform: translateY(0); + position: fixed; + width: 100%; + background: rgba(3, 6, 16, 0.95); + border-top: 1px solid var(--border); + } + + /* Canvas takes full space in immersive */ + .immersive #main { + display: block; + } + + .immersive #canvas { + position: fixed; + inset: 0; + z-index: 100; + background: #050a14; + } + + .immersive #panel-area { + position: absolute; + inset: 0; + } + + .immersive #bframe { + margin: 0; + border-radius: 0; + border: none; + box-shadow: none; + width: 100% !important; + max-width: 100% !important; + } + + .immersive #lp { + border-radius: 0; + } + + .immersive #preview-frame { + width: 100%; + height: 100%; + } + + /* Edge trigger zones — invisible strips at viewport edges */ + .edge-zone { + position: fixed; + z-index: 250; + background: transparent; + display: none; + } + + .immersive .edge-zone { + display: block; + } + + .edge-zone-top { + top: 0; + left: 0; + right: 0; + height: 6px; + cursor: n-resize; + } + + .edge-zone-bottom { + bottom: 0; + left: 0; + right: 0; + height: 6px; + cursor: s-resize; + } + + .edge-zone-left { + top: 0; + left: 0; + bottom: 0; + width: 6px; + cursor: w-resize; + } + + .edge-zone-right { + top: 0; + right: 0; + bottom: 0; + width: 6px; + cursor: e-resize; + } + + /* Edge zone hover indicator — subtle glow line */ + .edge-zone::after { + content: ''; + position: absolute; + background: rgba(147, 51, 234, 0); + transition: background .2s; + border-radius: 2px; + } + + .edge-zone-top::after, + .edge-zone-bottom::after { + left: 20%; + right: 20%; + height: 2px; + } + + .edge-zone-top::after { + top: 0; + } + + .edge-zone-bottom::after { + bottom: 0; + } + + .edge-zone-left::after, + .edge-zone-right::after { + top: 20%; + bottom: 20%; + width: 2px; + } + + .edge-zone-left::after { + left: 0; + } + + .edge-zone-right::after { + right: 0; + } + + .edge-zone:hover::after { + background: rgba(147, 51, 234, .6); + } + + /* Immersive exit button */ + .immersive-exit { + position: fixed; + top: 8px; + right: 8px; + z-index: 300; + width: 32px; + height: 32px; + border-radius: 8px; + background: rgba(0, 0, 0, .6); + border: 1px solid rgba(255, 255, 255, .12); + color: var(--t2); + font-size: 12px; + cursor: pointer; + display: none; + align-items: center; + justify-content: center; + backdrop-filter: blur(8px); + transition: all .2s; + opacity: .4; + } + + .immersive .immersive-exit { + display: flex; + } + + .immersive-exit:hover { + opacity: 1; + background: rgba(147, 51, 234, .35); + border-color: rgba(147, 51, 234, .5); + color: var(--t1); + transform: scale(1.1); + } + + /* Immersive enter button (shows in canvas toolbar) */ + .immersive-enter { + background: none; + border: none; + color: var(--t3); + cursor: pointer; + font-size: 10px; + padding: 4px 7px; + border-radius: 5px; + transition: all .15s; + display: flex; + align-items: center; + gap: 4px; + } + + .immersive-enter:hover { + color: var(--t1); + background: rgba(255, 255, 255, .06); + } + + /* ══════════════════════════════════════════════════════════ + APP MODE — streamlined view for games/apps/web + Content fills screen, NO overlapping, collapsible chat + Toggle: Ctrl+Shift+F or floating button + ══════════════════════════════════════════════════════════ */ + + /* Hide all dev tooling in app mode */ + .app-mode #topbar, + .app-mode #sb-wrap, + .app-mode #lsb, + .app-mode #pp, + .app-mode #pp-pull-tab, + .app-mode #rp, + .app-mode #ctb, + .app-mode #sugs, + .app-mode #tc, + .app-mode .frame-chrome, + .app-mode .btb, + .app-mode .edge-zone, + .app-mode .immersive-enter, + .app-mode .immersive-exit { + display: none !important; + } + + /* Togglable sub-elements: hide when their toggle is off */ + .app-mode.app-hide-status #ts { + display: none !important; + } + + .app-mode.app-hide-voice #dv-voice-bar { + display: none !important; + } + + .app-mode.app-hide-chat #cb, + .app-mode.app-hide-chat #chat-feed { + display: none !important; + } + + /* Full-screen layout: body becomes a flex column, content fills remaining space */ + .app-mode { + display: flex !important; + flex-direction: column !important; + } + + .app-mode #main { + flex: 1; + min-height: 0; + } + + .app-mode #canvas { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + } + + .app-mode #panel-area { + flex: 1; + position: relative; + min-height: 0; + } + + .app-mode #bframe { + margin: 0; + border-radius: 0; + border: none; + box-shadow: none; + width: 100% !important; + max-width: 100% !important; + flex: 1; + min-height: 0; + } + + .app-mode #lp { + border-radius: 0; + flex: 1; + min-height: 0; + overflow: hidden; + } + + .app-mode #preview-frame { + width: 100%; + height: 100%; + border: none; + } + + /* Chat — docked at bottom by default */ + .app-mode #chat { + flex-shrink: 0; + background: rgba(3, 6, 16, .95); + border-top: 1px solid rgba(255, 255, 255, .08); + padding: 0; + transition: max-height .25s ease; + position: relative; + } + + .app-mode #chat-feed { + max-height: clamp(120px, 22dvh, 260px) !important; + overflow-y: auto; + display: flex; + flex-direction: column; + border-bottom: 1px solid rgba(255, 255, 255, .05); + } + + .app-mode #chat-feed:empty { + display: none !important; + } + + .app-mode.has-chat-transcript #panel-area { + min-height: var(--coding-action-min-h); + } + + .app-mode.has-chat-transcript #bframe { + min-height: var(--coding-action-min-h); + } + + .app-mode.has-chat-transcript #chat { + max-height: min(44dvh, calc(var(--coding-transcript-max-h) + 112px)); + } + + .app-mode.has-chat-transcript #chat-feed { + display: flex !important; + min-height: var(--coding-transcript-min-h); + max-height: var(--coding-transcript-max-h) !important; + } + + .app-mode #ts { + padding: 3px 10px; + font-size: 10px; + } + + .app-mode #dv-voice-bar { + padding: 2px 8px; + font-size: 9px; + min-height: 20px; + border-bottom: 1px solid rgba(255, 255, 255, .04); + } + + .app-mode #cb { + padding: 4px 8px; + gap: 5px; + width: min(calc(100% - 16px), 1120px); + } + + .app-mode #cb textarea { + font-size: 12px; + max-height: 40px; + min-height: 18px; + padding: 3px 8px; + line-height: 1.3; + } + + /* ── Floating draggable chat window ── */ + .app-mode.app-chat-float #chat { + position: fixed !important; + z-index: 450; + bottom: 12px; + right: 12px; + left: auto; + top: auto; + width: 360px; + max-height: 340px; + min-width: 260px; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, .12); + box-shadow: 0 8px 32px rgba(0, 0, 0, .5); + overflow: hidden; + display: flex; + flex-direction: column; + resize: both; + background: rgba(3, 6, 16, .96); + backdrop-filter: blur(14px); + } + + .app-mode.app-chat-float #chat-feed { + max-height: 180px !important; + flex: 1; + min-height: 0; + } + + .app-mode.app-chat-float #cb textarea { + max-height: 60px; + } + + /* Chat float drag handle */ + #app-chat-drag { + display: none; + height: 22px; + cursor: grab; + flex-shrink: 0; + background: rgba(255, 255, 255, .04); + border-bottom: 1px solid rgba(255, 255, 255, .06); + align-items: center; + justify-content: space-between; + padding: 0 8px; + font-size: 9px; + color: var(--t3); + user-select: none; + } + + #app-chat-drag:active { + cursor: grabbing; + } + + .app-mode.app-chat-float #app-chat-drag { + display: flex; + } + + #app-chat-drag .chat-drag-dots { + display: flex; + gap: 2px; + opacity: .3; + } + + #app-chat-drag .chat-drag-dots span { + width: 3px; + height: 3px; + border-radius: 50%; + background: rgba(255, 255, 255, .5); + } + + #app-chat-drag .chat-drag-actions { + display: flex; + gap: 4px; + } + + #app-chat-drag .chat-drag-actions button { + width: 16px; + height: 16px; + border: none; + background: none; + color: var(--t3); + font-size: 8px; + cursor: pointer; + border-radius: 3px; + display: flex; + align-items: center; + justify-content: center; + } + + #app-chat-drag .chat-drag-actions button:hover { + background: rgba(255, 255, 255, .1); + color: #fff; + } + + /* ── Floating App Mode Toolbar (draggable) ── */ + #app-mode-toolbar { + position: fixed; + top: 8px; + right: 8px; + z-index: 500; + display: none; + align-items: center; + gap: 3px; + background: rgba(3, 6, 16, .88); + backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, .1); + border-radius: 10px; + padding: 3px 5px; + opacity: var(--amt-base-opacity, .35); + transition: opacity .2s; + user-select: none; + touch-action: none; + } + + #app-mode-toolbar:hover, + #app-mode-toolbar.dragging { + opacity: 1; + } + + .app-mode #app-mode-toolbar { + display: flex; + } + + /* Drag handle */ + .amt-drag { + width: 14px; + height: 24px; + cursor: grab; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + opacity: .3; + transition: opacity .15s; + flex-shrink: 0; + } + + .amt-drag:hover { + opacity: .7; + } + + .amt-drag:active { + cursor: grabbing; + } + + .amt-drag span { + display: block; + width: 10px; + height: 2px; + background: rgba(255, 255, 255, .4); + border-radius: 1px; + } + + .amt-btn { + width: 28px; + height: 28px; + border-radius: 7px; + border: 1px solid rgba(255, 255, 255, .08); + background: none; + color: var(--t2); + font-size: 11px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all .15s; + flex-shrink: 0; + } + + .amt-btn:hover { + background: rgba(147, 51, 234, .25); + border-color: rgba(147, 51, 234, .4); + color: #fff; + } + + .amt-btn.on { + background: rgba(147, 51, 234, .2); + border-color: rgba(147, 51, 234, .35); + color: #D8B4FE; + } + + .amt-btn.off { + opacity: .35; + } + + .amt-btn.mic-active { + background: rgba(239, 68, 68, .25); + border-color: rgba(239, 68, 68, .5); + color: #FCA5A5; + animation: amtMicPulse 1.2s ease-in-out infinite; + } + + @keyframes amtMicPulse { + + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(239, 68, 68, .3); + } + + 50% { + box-shadow: 0 0 8px 2px rgba(239, 68, 68, .4); + } + } + + .amt-sep { + width: 1px; + height: 18px; + background: rgba(255, 255, 255, .08); + margin: 0 2px; + flex-shrink: 0; + } + + .amt-zoom { + font-size: 9px; + color: var(--t2); + min-width: 32px; + text-align: center; + font-family: 'Courier New', monospace; + } + + /* ── Minimized chat in app mode ── */ + .app-mode.app-chat-minimized #chat { + max-height: 0; + overflow: hidden; + border: none; + padding: 0; + transition: max-height .25s ease; + } + + .app-mode.app-chat-minimized #dv-voice-bar, + .app-mode.app-chat-minimized #cb, + .app-mode.app-chat-minimized #chat-feed, + .app-mode.app-chat-minimized #ts { + display: none !important; + } + + /* Mini chat indicator when minimized */ + #app-chat-mini-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 200; + height: 22px; + background: rgba(3, 6, 16, .92); + border-top: 1px solid rgba(255, 255, 255, .06); + display: none; + align-items: center; + padding: 0 10px; + cursor: pointer; + font-size: 9px; + color: var(--t3); + gap: 6px; + transition: all .15s; + } + + #app-chat-mini-bar:hover { + background: rgba(147, 51, 234, .12); + color: var(--t1); + } + + .app-mode.app-chat-minimized #app-chat-mini-bar { + display: flex; + } + + /* ── App Mode Enter Button (visible in normal mode) ── */ + #app-mode-enter { + position: fixed; + bottom: 10px; + right: 10px; + z-index: 300; + height: 30px; + border-radius: 8px; + padding: 0 10px; + background: rgba(8, 14, 28, .85); + border: 1px solid rgba(255, 255, 255, .1); + color: var(--t2); + font-size: 10px; + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; + backdrop-filter: blur(8px); + transition: all .2s; + opacity: .45; + font-family: var(--fb); + } + + #app-mode-enter:hover { + opacity: 1; + background: rgba(147, 51, 234, .25); + border-color: rgba(147, 51, 234, .4); + color: #fff; + } + + .app-mode #app-mode-enter { + display: none !important; + } + + /* Zoom transform on the preview frame */ + .app-mode #preview-frame { + transform-origin: 0 0; + } + + /* ══════════════════════════════════════════════════════════ + RESPONSIVE — COMPACT DESKTOP (≤1536px) + ══════════════════════════════════════════════════════════ */ + @media (max-width: 1536px) { + .pill-sub, + #tb-clock { + display: none !important; + } + + .agent-pills { + gap: 1px; + } + + .pill { + padding: 4px 5px; + } + + .top-device-switcher { + padding-inline: 2px !important; + } + + .ptab { + padding-inline: 6px; + } + } + + /* ══════════════════════════════════════════════════════════ + RESPONSIVE — TABLET (Tab S9+, ≤1024px) + ══════════════════════════════════════════════════════════ */ + @media (max-width: 1024px) { + #rp { + width: 0; + min-width: 0; + max-width: 0; + border-left: none; + } + + #rp-resizer { + display: none; + } + + #rp-tb { + display: none; + } + + .layout-btn { + display: none; + } + + .curl { + display: none; + } + + .dm-indicator { + display: none; + } + + #ctb .ctab.dev-mode { + display: none; + } + + #ctb .vdiv { + display: none; + } + + .pill-sub { + display: none; + } + + .pill { + padding: 3px 6px; + font-size: 10px; + } + + #topbar { + padding: 0 6px; + gap: 4px; + } + + .tb-right { + gap: 2px; + } + + #tb-clock { + display: none !important; + } + + #pp { + width: 220px; + } + + #bframe { + margin: 4px 4px 0; + } + + .dm-desktop #bframe { + margin: 4px 4px 0; + } + } + + /* ══════════════════════════════════════════════════════════ + RESPONSIVE — MOBILE (S21 Ultra, ≤600px) + ══════════════════════════════════════════════════════════ */ + @media (max-width: 600px) { + :root { + --topbar-h: 40px; + --modebar-h: 32px; + --transcript-max-h: clamp(130px, 28dvh, 220px); + --action-window-min-h: 220px; + } + + /* Hide sidebar rail on mobile — too narrow */ + #lsb { + display: none; + } + + /* Show mobile menu button */ + #mobile-menu-btn { + display: block !important; + } + + /* Right panel always collapsed */ + #rp { + width: 0; + min-width: 0; + max-width: 0; + border-left: none; + } + + /* Fly-out panel: full width overlay on mobile */ + #pp { + left: 0; + width: 100vw; + border-right: none; + z-index: 60; + } + + /* Header: compact */ + #topbar { + height: 40px; + padding: 0 6px; + gap: 3px; + min-width: 0; + overflow: hidden; + } + + .logo-text { + display: none; + } + + .agent-pills { + display: none; + } + + .vdiv { + display: none; + } + + .tb-right { + gap: 2px; + margin-left: auto; + flex-shrink: 0; + } + + .tb-btn { + width: 26px; + height: 26px; + } + + #tb-clock { + display: none !important; + } + + .layout-btn { + display: none; + } + + /* Mobile gets its own preview toolbar below; desktop topbar controls bunch the S21 layout. */ + .top-device-switcher, + #topbar .panel-tabs, + #topbar .api-status, + #admin-badge, + .avatar-name { + display: none !important; + } + + #mode-badge { + min-width: 0; + max-width: 104px; + flex-shrink: 1; + } + + #mode-badge .mode-label { + max-width: 54px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .panel-tabs .ptab { + font-size: 9px; + padding: 3px 6px; + } + + .panel-tabs .ptab span { + display: none; + } + + .api-status span { + display: none; + } + + /* Canvas toolbar: hide device mode buttons */ + #ctb .ctab.dev-mode { + display: none; + } + + #ctb .vdiv { + display: none; + } + + .curl { + display: none; + } + + .dm-indicator { + display: none; + } + + #ctb { + height: 32px; + padding: 0 4px; + min-width: 0; + overflow: hidden; + } + + /* Preview frame: tighter margins */ + #bframe { + margin: 2px 2px 0; + border-radius: 6px; + } + + .dm-desktop #bframe { + margin: 2px 2px 0; + } + + #main, + #canvas, + #panel-area { + min-width: 0; + width: 100%; + } + + #bframe, + .dm-desktop #bframe, + .dm-tablet #bframe, + .dm-android #bframe, + .dm-mobile #bframe { + width: auto !important; + max-width: none !important; + min-width: 0; + margin: 2px 2px 0; + } + + #lp { + overflow-x: hidden; + } + + #demo-hero { + padding: 18px 16px; + gap: 9px; + overflow-y: auto; + justify-content: center; + } + + .demo-cat-badge { + padding: 4px 10px; + font-size: 8.5px; + } + + .demo-title { + font-size: clamp(24px, 9vw, 34px); + line-height: 1.06; + } + + .demo-sub { + font-size: 11.5px; + line-height: 1.45; + max-width: 330px; + } + + .demo-actions { + gap: 8px; + margin-top: 6px; + } + + .demo-btn-play, + .demo-btn-create { + min-width: min(240px, calc(100vw - 96px)); + justify-content: center; + padding: 11px 18px; + border-radius: 12px; + font-size: 13px; + } + + .demo-meta { + font-size: 8.5px; + gap: 5px; + margin-top: 2px; + } + + #game-carousel { + bottom: 38px; + width: calc(100% - 28px); + gap: 8px; + } + + .carousel-container { + gap: 8px; + justify-content: center; + } + + .carousel-arrow { + width: 34px; + height: 34px; + } + + .carousel-track { + flex: 0 1 min(236px, calc(100vw - 118px)); + height: 82px; + } + + .game-card { + min-width: min(236px, calc(100vw - 118px)); + height: 78px; + padding: 9px 11px; + } + + .btb { + height: 28px; + padding: 0 6px; + } + + /* Status bar: hide on mobile to save space */ + #sb-wrap { + display: none !important; + } + + #sb-wrap>div:first-child { + padding: 0 6px; + } + + #sb-wrap>div:first-child span:last-child { + display: none; + } + + #sb-ts { + display: none; + } + + /* Chat area: full width, comfortable padding */ + #pc { + padding: 24px 12px 20px; + } + + .ph { + max-width: 100%; + } + + .p-title { + font-size: 22px; + } + + .p-sub { + font-size: 12px; + } + + .sug { + font-size: 10px; + padding: 8px 10px; + } + + #cb { + margin: 0 4px 4px; + width: calc(100% - 8px); + max-width: none; + gap: 4px; + padding: 6px 6px 4px; + } + + #chat { + max-height: clamp(150px, 34dvh, 280px); + overflow: hidden; + padding-bottom: 4px; + } + + #chat-feed { + max-height: var(--transcript-max-h) !important; + overflow: auto; + } + + body.has-chat-transcript { + --coding-action-min-h: clamp(220px, 42dvh, 380px); + --coding-transcript-min-h: clamp(110px, 20dvh, 180px); + --coding-transcript-max-h: clamp(150px, 28dvh, 240px); + } + + body.has-chat-transcript #chat-feed { + min-height: var(--coding-transcript-min-h); + max-height: var(--coding-transcript-max-h) !important; + } + + #sugs { + display: none; + } + + #pi { + min-height: 34px; + max-height: 70px; + padding: 7px 9px; + font-size: 12px; + } + + .cib { + width: 28px; + height: 28px; + flex-shrink: 0; + } + + .sbtn { + width: 32px; + height: 32px; + flex-shrink: 0; + } + + #dv-voice-bar { + width: calc(100% - 8px); + max-width: none; + margin: 0 4px 2px; + padding: 3px 6px; + gap: 4px; + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; + scrollbar-width: none; + } + + #dv-voice-bar::-webkit-scrollbar { + display: none; + } + + #dv-voice-bar .dvb-engine, + #dv-voice-bar .dvb-voice, + #dv-voice-bar #dvb-think-label, + #dv-voice-bar #dvb-chatmode-label, + #dvb-hint { + display: none !important; + } + + #dv-voice-bar .dvb-mode, + #dv-voice-bar .dvb-btn { + flex-shrink: 0; + } + + #app-mode-enter { + bottom: 100px; + right: 8px; + width: 34px; + height: 34px; + padding: 0; + justify-content: center; + border-radius: 10px; + font-size: 0; + opacity: .65; + } + + #app-mode-enter i { + font-size: 12px !important; + } + + /* Auth modal */ + .am-box { + width: 92vw; + max-width: 360px; + padding: 24px 18px; + } + + /* Settings modal */ + #settings-ov .stg-box { + width: 92vw; + max-width: 360px; + } + + /* Tool modal */ + .tm-box { + width: 95vw; + max-width: none; + height: 70vh; + } + + /* Layout picker */ + .lgrid { + max-width: 100%; + padding: 2px; + } + + .lcard { + min-width: 140px; + } + } + + /* ══════════════════════════════════════════════════════════ + RESPONSIVE — SMALL MOBILE (≤380px) + ══════════════════════════════════════════════════════════ */ + @media (max-width: 380px) { + #topbar { + height: 36px; + } + + .logo-icon { + width: 22px; + height: 22px; + font-size: 10px; + } + + .panel-tabs .ptab { + padding: 2px 4px; + font-size: 8px; + } + + .avatar { + width: 24px; + height: 24px; + font-size: 9px; + } + + #sb-wrap { + display: none !important; + } + + #pc { + padding: 16px 8px 12px; + } + + .p-title { + font-size: 18px; + } + } + + /* ══ MODE-AWARE UI (Phase 3 + 5 — spec wave2-handoffs/08) ══ */ + :root { + --mode-chat: #A78BFA; + --mode-web: #60A5FA; + --mode-build: #8B5CF6; + --mode-code: #3B82F6; + --mode-files: #22D3EE; + --mode-research: #14B8A6; + --mode-problem: #8B5CF6; + --mode-game: #EC4899; + --mode-hermes: #10B981; + --mode-admin: #F59E0B; + --mode-active: var(--mode-build); + --mode-active-g: rgba(139, 92, 246, .4); + --mode-active-tint: rgba(139, 92, 246, .08); + } + + body[data-mode="chat"] { --mode-active: #A78BFA; --mode-active-g: rgba(167,139,250,.4); --mode-active-tint: rgba(167,139,250,.08); } + body[data-mode="web"] { --mode-active: #60A5FA; --mode-active-g: rgba(96,165,250,.4); --mode-active-tint: rgba(96,165,250,.08); } + body[data-mode="build"] { --mode-active: #8B5CF6; --mode-active-g: rgba(139,92,246,.4); --mode-active-tint: rgba(139,92,246,.08); } + body[data-mode="code"] { --mode-active: #3B82F6; --mode-active-g: rgba(59,130,246,.4); --mode-active-tint: rgba(59,130,246,.08); } + body[data-mode="files"] { --mode-active: #22D3EE; --mode-active-g: rgba(34,211,238,.4); --mode-active-tint: rgba(34,211,238,.08); } + body[data-mode="research"] { --mode-active: #14B8A6; --mode-active-g: rgba(20,184,166,.4); --mode-active-tint: rgba(20,184,166,.08); } + body[data-mode="problem"] { --mode-active: #8B5CF6; --mode-active-g: rgba(139,92,246,.4); --mode-active-tint: rgba(139,92,246,.08); } + body[data-mode="game"] { --mode-active: #EC4899; --mode-active-g: rgba(236,72,153,.4); --mode-active-tint: rgba(236,72,153,.08); } + body[data-mode="hermes"] { --mode-active: #10B981; --mode-active-g: rgba(16,185,129,.4); --mode-active-tint: rgba(16,185,129,.08); } + body[data-mode="admin"] { --mode-active: #F59E0B; --mode-active-g: rgba(245,158,11,.4); --mode-active-tint: rgba(245,158,11,.08); } + + /* Mode badge in topbar */ + #mode-badge { + display: inline-flex; + align-items: center; + gap: 6px; + height: 24px; + padding: 0 10px 0 8px; + border-radius: 14px; + background: var(--mode-active-tint); + border: 1px solid var(--mode-active); + color: var(--mode-active); + font-family: var(--fb); + font-size: 10.5px; + font-weight: 600; + letter-spacing: .02em; + cursor: pointer; + user-select: none; + transition: background-color 300ms ease, color 300ms ease, border-color 300ms ease; + white-space: nowrap; + } + #mode-badge:hover { + background: var(--mode-active); + color: #fff; + } + #mode-badge .mode-icon { + font-size: 11px; + line-height: 1; + } + #mode-badge .mode-caret { + opacity: .6; + font-size: 8px; + margin-left: 2px; + } + #mode-badge.locked::after { + content: "🔒"; + font-size: 8px; + margin-left: 2px; + opacity: .8; + } + + /* Mode picker dropdown */ + #mode-picker { + position: absolute; + top: 48px; + left: 8px; + min-width: 220px; + background: var(--bg-elevated); + backdrop-filter: blur(24px); + border: 1px solid var(--border-hi); + border-radius: 10px; + box-shadow: 0 16px 40px rgba(0, 0, 0, .55); + padding: 6px; + z-index: 200; + display: none; + font-family: var(--fb); + } + #mode-picker.open { display: block; } + #mode-picker .mp-item { + display: flex; + align-items: center; + gap: 9px; + padding: 7px 10px; + border-radius: 7px; + cursor: pointer; + font-size: 11px; + color: var(--t1); + transition: background-color 120ms ease; + } + #mode-picker .mp-item:hover { background: rgba(255, 255, 255, .06); } + #mode-picker .mp-item.cur { background: rgba(255, 255, 255, .08); } + #mode-picker .mp-item .mp-dot { + width: 9px; + height: 9px; + border-radius: 50%; + box-shadow: 0 0 6px currentColor; + } + #mode-picker .mp-item .mp-check { + margin-left: auto; + opacity: .85; + font-size: 9px; + } + #mode-picker .mp-foot { + border-top: 1px solid var(--border); + margin-top: 4px; + padding: 6px 10px; + font-size: 9px; + color: var(--t3); + line-height: 1.5; + } + + /* Mode-pane container — overlays the existing #panel-area for non-build modes */ + .mode-pane { + display: none; + flex: 1; + flex-direction: column; + overflow: hidden; + padding: 0; + } + .mode-pane.vis { display: flex; } + + .mode-stub { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 28px; + overflow-y: auto; + } + .mode-stub-card { + max-width: 560px; + width: 100%; + background: rgba(255, 255, 255, .03); + border: 1px solid var(--border-hi); + border-radius: 14px; + padding: 22px 24px; + color: var(--t1); + font-family: var(--fb); + } + .mode-stub-card h3 { + font-size: 15px; + font-weight: 700; + margin: 0 0 6px; + color: var(--mode-active); + font-family: var(--fd); + letter-spacing: .01em; + } + .mode-stub-card .ms-sub { + font-size: 11px; + color: var(--t2); + margin-bottom: 14px; + line-height: 1.5; + } + .mode-stub-card ul { + list-style: none; + padding: 0; + margin: 0 0 14px; + font-size: 11px; + color: var(--t1); + } + .mode-stub-card li { + padding: 6px 0; + border-bottom: 1px solid rgba(255, 255, 255, .04); + display: flex; + align-items: center; + gap: 8px; + } + .mode-stub-card li:last-child { border-bottom: none; } + .mode-stub-card li::before { + content: "•"; + color: var(--mode-active); + font-size: 14px; + line-height: 0; + } + .mode-stub-card .ms-link { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 10.5px; + color: var(--mode-active); + text-decoration: none; + padding: 6px 10px; + border-radius: 6px; + background: var(--mode-active-tint); + border: 1px solid var(--mode-active); + } + .mode-stub-card .ms-link:hover { background: var(--mode-active); color: #fff; } + .mode-stub-card .ms-urlbar { + display: flex; + gap: 6px; + margin: 0 0 12px; + } + .mode-stub-card .ms-urlbar input { + flex: 1; + background: rgba(255, 255, 255, .04); + border: 1px solid var(--border-hi); + border-radius: 6px; + color: var(--t1); + padding: 7px 10px; + font-size: 11px; + font-family: inherit; + outline: none; + } + .mode-stub-card .ms-urlbar input:focus { border-color: var(--mode-active); } + .mode-stub-card .ms-urlbar button { + background: var(--mode-active); + color: #fff; + border: none; + border-radius: 6px; + padding: 7px 14px; + font-size: 11px; + font-weight: 600; + cursor: pointer; + } + + @media (prefers-reduced-motion: reduce) { + #mode-badge { transition: none; } + } + + /* ── Codex/Claude-Code Parity Phase A1 — Command Palette, Slash Commands, Files Tree ── */ + /* Command Palette overlay */ + #cmd-palette-ov { + position: fixed; inset: 0; background: rgba(0,0,0,.55); + display: none; align-items: flex-start; justify-content: center; + padding-top: 12vh; z-index: 10500; + backdrop-filter: blur(4px); + } + #cmd-palette-ov.open { display: flex; } + #cmd-palette { + width: min(640px, 92vw); max-height: 70vh; + background: #0f1424; border: 1px solid rgba(139,92,246,.25); + border-radius: 10px; box-shadow: 0 24px 60px rgba(0,0,0,.55); + display: flex; flex-direction: column; overflow: hidden; + font-family: inherit; + } + #cmd-palette-input { + width: 100%; padding: 14px 16px; background: transparent; + border: none; outline: none; color: #fff; font-size: 14px; + border-bottom: 1px solid rgba(255,255,255,.06); + } + #cmd-palette-input::placeholder { color: rgba(255,255,255,.35); } + #cmd-palette-list { + flex: 1; overflow-y: auto; padding: 4px 0; + } + .cp-item { + display: flex; align-items: center; gap: 10px; + padding: 8px 14px; cursor: pointer; color: #cbd5e1; + font-size: 12.5px; border-left: 2px solid transparent; + } + .cp-item:hover, .cp-item.cp-active { + background: rgba(139,92,246,.12); color: #fff; + border-left-color: var(--mode-active, #8B5CF6); + } + .cp-item-icon { + width: 18px; text-align: center; color: var(--mode-active, #8B5CF6); + font-size: 11px; flex-shrink: 0; + } + .cp-item-label { flex: 1; } + .cp-item-hint { + font-size: 10px; color: rgba(255,255,255,.4); + padding: 1px 6px; border: 1px solid rgba(255,255,255,.1); + border-radius: 3px; font-family: ui-monospace, monospace; + } + .cp-section { + padding: 6px 14px 4px; font-size: 9.5px; + color: rgba(255,255,255,.4); text-transform: uppercase; + letter-spacing: .8px; + } + #cmd-palette-foot { + padding: 6px 14px; border-top: 1px solid rgba(255,255,255,.06); + font-size: 10px; color: rgba(255,255,255,.45); + display: flex; gap: 12px; + } + + /* Slash command dropdown above chat input */ + #slash-dd { + position: absolute; left: 0; right: 0; bottom: 100%; + margin-bottom: 6px; background: #0f1424; + border: 1px solid rgba(139,92,246,.3); border-radius: 8px; + box-shadow: 0 -8px 24px rgba(0,0,0,.4); + max-height: 260px; overflow-y: auto; display: none; + z-index: 1200; + } + #slash-dd.open { display: block; } + .sl-item { + display: flex; align-items: center; gap: 8px; + padding: 7px 12px; cursor: pointer; color: #cbd5e1; + font-size: 12px; border-left: 2px solid transparent; + } + .sl-item:hover, .sl-item.sl-active { + background: rgba(139,92,246,.13); color: #fff; + border-left-color: var(--mode-active, #8B5CF6); + } + .sl-item-cmd { + font-family: ui-monospace, monospace; color: #93C5FD; + font-weight: 600; min-width: 90px; + } + .sl-item-desc { + flex: 1; font-size: 11px; color: rgba(255,255,255,.55); + } + .sl-item-locked { opacity: .55; cursor: not-allowed; } + .sl-item-locked .sl-item-cmd { color: #fbbf24; } + + /* Files mini-tree (mode-pane-files content) */ + #mode-pane-files .ft-wrap { + display: flex; flex-direction: column; width: 100%; height: 100%; + background: #0a0e1a; color: #cbd5e1; + } + #mode-pane-files .ft-bar { + display: flex; gap: 6px; padding: 8px 10px; + border-bottom: 1px solid rgba(255,255,255,.06); + background: #0f1424; + } + #mode-pane-files .ft-filter { + flex: 1; padding: 5px 9px; background: rgba(255,255,255,.04); + border: 1px solid rgba(255,255,255,.08); border-radius: 4px; + color: #fff; font-size: 11px; outline: none; + } + #mode-pane-files .ft-filter:focus { border-color: var(--mode-active, #8B5CF6); } + #mode-pane-files .ft-root-select { + padding: 5px 8px; background: rgba(255,255,255,.04); + border: 1px solid rgba(255,255,255,.08); border-radius: 4px; + color: #fff; font-size: 11px; outline: none; cursor: pointer; + } + #mode-pane-files .ft-refresh { + padding: 5px 9px; background: rgba(139,92,246,.18); + border: 1px solid rgba(139,92,246,.4); border-radius: 4px; + color: #fff; font-size: 11px; cursor: pointer; + } + #mode-pane-files .ft-body { + display: flex; flex: 1; min-height: 0; + } + #mode-pane-files .ft-tree { + width: 38%; min-width: 220px; + overflow-y: auto; padding: 6px 0; + border-right: 1px solid rgba(255,255,255,.06); + font-family: ui-monospace, monospace; font-size: 11.5px; + } + #mode-pane-files .ft-node { + padding: 3px 8px 3px 0; cursor: pointer; user-select: none; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + } + #mode-pane-files .ft-node:hover { background: rgba(34,211,238,.10); } + #mode-pane-files .ft-node.ft-sel { background: rgba(34,211,238,.20); color: #fff; } + #mode-pane-files .ft-icon { display: inline-block; width: 14px; color: #94a3b8; } + #mode-pane-files .ft-dir { color: #93C5FD; } + #mode-pane-files .ft-file { color: #cbd5e1; } + #mode-pane-files .ft-empty { padding: 12px; color: rgba(255,255,255,.4); font-size: 11px; } + #mode-pane-files .ft-view { + flex: 1; overflow: auto; padding: 10px 12px; + background: #060916; font-family: ui-monospace, monospace; + font-size: 11.5px; line-height: 1.5; color: #e2e8f0; + white-space: pre-wrap; word-break: break-word; + } + #mode-pane-files .ft-view-empty { + color: rgba(255,255,255,.4); font-family: inherit; + } + #mode-pane-files .ft-view-head { + font-family: inherit; font-size: 10.5px; color: var(--mode-active, #8B5CF6); + padding-bottom: 6px; margin-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,.06); + } + + /* ══ MOBILE TOUCH TARGET FIX (S21 Ultra audit) ══ */ + /* Voice bar buttons (action, voice, auto, settings) — enforce 44px minimum touch target */ + #dv-voice-bar .dvb-mode, + #dv-voice-bar .dvb-engine, + #dv-voice-bar .dvb-voice, + #dv-voice-bar .dvb-btn { + min-height: 44px; + min-width: 44px; + } + + /* Composer buttons (screenshot, code, mic, send) — enforce 44px minimum touch target */ + #cb .cib { + min-height: 44px; + min-width: 44px; + } + + /* Side-chat workspace presets keep the full composer usable in a narrow column. */ + @media (min-width: 1025px) { + body.workspace-chat-side-right #dv-voice-bar, + body.workspace-chat-side-left #dv-voice-bar { + width: calc(100% - 16px); + max-width: none; + overflow: hidden; + gap: 5px; + } + + body.workspace-chat-side-right #dv-voice-bar .dvb-engine, + body.workspace-chat-side-left #dv-voice-bar .dvb-engine, + body.workspace-chat-side-right #dv-voice-bar .dvb-voice, + body.workspace-chat-side-left #dv-voice-bar .dvb-voice, + body.workspace-chat-side-right #dv-voice-bar .dvb-sep, + body.workspace-chat-side-left #dv-voice-bar .dvb-sep, + body.workspace-chat-side-right #dvb-think-label, + body.workspace-chat-side-left #dvb-think-label, + body.workspace-chat-side-right #dvb-chatmode-label, + body.workspace-chat-side-left #dvb-chatmode-label, + body.workspace-chat-side-right #dvb-hint, + body.workspace-chat-side-left #dvb-hint { + display: none !important; + } + + body.workspace-chat-side-right #dv-voice-bar .dvb-mode, + body.workspace-chat-side-left #dv-voice-bar .dvb-mode { + flex: 1 1 auto; + min-width: 0; + justify-content: center; + } + + body.workspace-chat-side-right #dv-voice-bar .dvb-btn, + body.workspace-chat-side-left #dv-voice-bar .dvb-btn { + flex: 0 0 44px; + width: 44px; + min-width: 44px !important; + padding-inline: 0; + text-align: center; + } + + body.workspace-chat-side-right #dvb-chatmode, + body.workspace-chat-side-left #dvb-chatmode { + min-width: 44px !important; + } + + body.workspace-chat-side-right #cb, + body.workspace-chat-side-left #cb { + gap: 4px; + padding-inline: 8px; + } + + body.workspace-chat-side-right #pi, + body.workspace-chat-side-left #pi { + min-width: 0; + padding-inline: 9px; + } + + body.workspace-chat-side-right #cb .csep, + body.workspace-chat-side-left #cb .csep { + display: none; + } + + body.workspace-chat-side-right #app-mode-enter { + right: calc(var(--workspace-chat-side-w) + 10px); + } + } + + /* ══════════════════════════════════════════════════════════ + COMPLETE DEVICE WORKSPACES + Device selection adapts the full builder shell, not only the iframe. + ══════════════════════════════════════════════════════════ */ + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) { + --right-panel-max-w: min(72vw, 420px); + --transcript-max-h: clamp(90px, 19dvh, 170px); + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #main { + position: relative; + min-width: 0; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #canvas { + order: 1 !important; + display: flex !important; + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + width: 100%; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #ctb, + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #panel-area, + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #chat { + grid-column: auto !important; + grid-row: auto !important; + min-width: 0; + width: 100%; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #panel-area { + flex: 1 1 auto; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #chat { + flex: 0 0 auto; + border-right: 0; + border-left: 0; + border-top: 1px solid rgba(var(--workspace-primary-rgb), .13); + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #rp { + position: fixed; + top: calc(var(--topbar-h) + var(--statusbar-h)); + right: 0; + bottom: 0; + left: auto; + z-index: 850; + order: 3 !important; + width: min(360px, 72vw) !important; + min-width: min(320px, 72vw) !important; + max-width: min(420px, 78vw) !important; + height: auto; + border: 1px solid rgba(var(--workspace-primary-rgb), .18); + border-right: 0; + border-radius: 12px 0 0 12px; + box-shadow: -18px 0 48px rgba(0, 0, 0, .48); + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #rp.collapsed { + width: 0 !important; + min-width: 0 !important; + max-width: 0 !important; + border: 0; + box-shadow: none; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #rp-resizer { + display: none !important; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #rp-tb { + display: flex !important; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) .layout-btn { + display: flex !important; + width: 28px; + min-width: 28px; + flex: 0 0 28px; + justify-content: center; + gap: 0; + padding: 0; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) .layout-btn #ln, + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) .layout-btn .fa-chevron-down { + display: none !important; + } + + body[data-device-mode="tablet"] { + --topbar-h: 42px; + --modebar-h: 34px; + --composer-h: 46px; + } + + body[data-device-mode="tablet"] #topbar { + height: var(--topbar-h); + padding-inline: 7px; + gap: 4px; + } + + body[data-device-mode="tablet"] .pill-sub, + body[data-device-mode="tablet"] #tb-clock, + body[data-device-mode="tablet"] .avatar-name { + display: none !important; + } + + body[data-device-mode="tablet"] .agent-pills { + gap: 1px; + } + + body[data-device-mode="tablet"] .pill { + padding: 3px 5px; + } + + body[data-device-mode="tablet"] #lsb { + width: 44px; + min-width: 44px; + } + + body[data-device-mode="tablet"] #pp, + body[data-device-mode="tablet"] #pp-pull-tab { + left: 44px; + } + + body[data-device-mode="tablet"] #pp { + width: min(300px, 52vw); + } + + body[data-device-mode="tablet"] #chat { + max-height: clamp(150px, 25dvh, 210px); + } + + body[data-device-mode="tablet"] #bframe { + width: 768px; + max-width: calc(100% - 16px); + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) { + --topbar-h: 40px; + --statusbar-h: 0px; + --modebar-h: 32px; + --composer-h: 44px; + --transcript-max-h: clamp(80px, 16dvh, 140px); + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #topbar { + height: var(--topbar-h); + min-width: 0; + padding: 0 6px; + gap: 3px; + overflow: visible; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .logo-text, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .agent-pills, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #topbar > .vdiv, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #topbar .panel-tabs, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #topbar .api-status, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #tb-clock, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .avatar-name, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #admin-badge { + display: none !important; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #mobile-menu-btn { + display: block !important; + flex: 0 0 auto; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #mode-badge { + max-width: 94px; + min-width: 0; + padding-inline: 7px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #mode-badge .mode-label { + max-width: 46px; + overflow: hidden; + text-overflow: ellipsis; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .top-device-switcher { + display: flex !important; + gap: 1px !important; + padding-inline: 1px !important; + margin-left: auto; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .top-device-switcher .ctab { + display: inline-flex !important; + align-items: center; + justify-content: center; + width: 25px; + height: 25px; + padding: 0; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .tb-right { + margin-left: 0; + gap: 3px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #rp-tb, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .avatar { + width: 28px; + height: 28px; + min-width: 28px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #sb-wrap, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #lsb, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #pp-pull-tab { + display: none !important; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #pp { + position: fixed; + top: var(--topbar-h); + bottom: 0; + left: 0; + width: min(100vw, 420px); + z-index: 900; + border-right: 1px solid rgba(var(--workspace-primary-rgb), .2); + box-shadow: 18px 0 48px rgba(0, 0, 0, .5); + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #rp { + top: var(--topbar-h); + width: min(92vw, 360px) !important; + min-width: min(320px, 92vw) !important; + max-width: min(92vw, 360px) !important; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #ctb { + height: var(--modebar-h); + padding-inline: 5px; + overflow: hidden; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #ctb .ctab.dev-mode, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #ctb .vdiv, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .curl { + display: none !important; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #chat { + max-height: clamp(142px, 24dvh, 180px); + padding-bottom: 4px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #sugs, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dvb-hint, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar .dvb-engine, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar .dvb-voice, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar .dvb-sep, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dvb-think-label, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dvb-chatmode-label { + display: none !important; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar { + width: calc(100% - 8px); + max-width: none; + margin: 0 4px 2px; + padding: 3px 6px; + gap: 4px; + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; + scrollbar-width: none; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar::-webkit-scrollbar { + display: none; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar .dvb-mode, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar .dvb-btn { + flex-shrink: 0; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #cb { + width: calc(100% - 8px); + max-width: none; + margin-inline: 4px; + padding: 4px 6px; + gap: 4px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #pi { + min-width: 0; + min-height: 36px; + padding: 7px 9px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #bframe { + max-width: calc(100% - 8px); + margin: 4px auto 0; + } + + body[data-device-mode="android"] #bframe { + width: 412px; + } + + body[data-device-mode="mobile"] #bframe { + width: 375px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #app-mode-enter { + right: 8px; + bottom: 96px; + width: 34px; + height: 34px; + padding: 0; + justify-content: center; + font-size: 0; + } + + /* Responsive showcase content inside simulated Tablet/Phone frames. */ + body[data-device-mode="tablet"] #demo-hero { + justify-content: center; + padding: 22px 22px 136px; + gap: 8px; + overflow: hidden; + } + + body[data-device-mode="tablet"] .demo-title { + font-size: clamp(30px, 4vw, 40px); + } + + body[data-device-mode="tablet"] .demo-sub { + max-width: 500px; + font-size: 12.5px; + line-height: 1.45; + } + + body[data-device-mode="tablet"] .demo-actions { + margin-top: 5px; + gap: 10px; + } + + body[data-device-mode="tablet"] .demo-btn-play, + body[data-device-mode="tablet"] .demo-btn-create { + padding: 11px 24px; + border-radius: 11px; + font-size: 13px; + } + + body[data-device-mode="tablet"] #game-carousel { + bottom: 12px; + width: calc(100% - 32px); + gap: 7px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #demo-hero { + justify-content: center; + padding: 14px 14px 116px; + gap: 7px; + overflow: hidden; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-cat-badge { + padding: 4px 10px; + font-size: 8px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-title { + width: 100%; + max-width: 332px; + font-size: 30px; + line-height: 1.04; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-sub { + max-width: 320px; + font-size: 11px; + line-height: 1.38; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-actions { + width: 100%; + max-width: 326px; + flex-wrap: nowrap; + gap: 7px; + margin-top: 3px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-btn-play, + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-btn-create { + flex: 1 1 0; + min-width: 0; + justify-content: center; + padding: 10px 7px; + border-radius: 10px; + font-size: 11.5px; + white-space: nowrap; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-btn-play i { + font-size: 12px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-meta { + display: none; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) #game-carousel { + bottom: 10px; + width: calc(100% - 18px); + gap: 6px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .carousel-container { + gap: 6px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .carousel-arrow { + width: 32px; + height: 32px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .carousel-track { + flex: 1 1 auto; + min-width: 0; + height: 74px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .game-card { + min-width: 100%; + height: 72px; + padding: 8px 10px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]) .carousel-cards { + gap: 6px; + } + + /* Responsive preset personalities: preserve each layout's workspace + structure instead of reducing the presets to colour changes. */ + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #ctb { + order: 0; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #panel-area { + order: 1; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #chat { + order: 2; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-side-left #chat { + order: 1; + border-top-color: rgba(var(--workspace-secondary-rgb), .28); + border-bottom: 1px solid rgba(var(--workspace-secondary-rgb), .18); + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-side-left #panel-area { + order: 2; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-side-right #chat { + border-top-color: rgba(var(--workspace-secondary-rgb), .28); + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]).workspace-rp-left #rp { + right: auto; + left: 0; + border-right: 1px solid rgba(var(--workspace-primary-rgb), .18); + border-left: 0; + border-radius: 0 12px 12px 0; + box-shadow: 18px 0 48px rgba(0, 0, 0, .48); + } + + body.workspace-rp-hidden #rp-tb { + display: none !important; + } + + body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]):not(.workspace-rp-hidden) #rp { + width: min(var(--workspace-activity-w, 360px), 92vw) !important; + min-width: min(300px, 86vw) !important; + max-width: min(420px, 92vw) !important; + } + + body[data-device-mode="tablet"] .layout-btn { + width: auto; + max-width: 126px; + flex-basis: auto; + gap: 4px; + padding-inline: 8px; + } + + body[data-device-mode="tablet"] .layout-btn #ln, + body[data-device-mode="tablet"] .layout-btn .fa-chevron-down { + display: inline !important; + } + + body[data-device-mode="tablet"] .layout-btn #ln { + max-width: 82px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + body[data-device-mode="tablet"].workspace-rail-wide #lsb { + width: 52px; + min-width: 52px; + } + + body[data-device-mode="tablet"].workspace-rail-wide #pp, + body[data-device-mode="tablet"].workspace-rail-wide #pp-pull-tab { + left: 52px; + } + + body[data-device-mode="tablet"].workspace-rail-hidden #lsb { + display: none !important; + } + + body[data-device-mode="tablet"].workspace-rail-hidden #pp, + body[data-device-mode="tablet"].workspace-rail-hidden #pp-pull-tab { + left: 0; + } + + body[data-device-mode="tablet"].workspace-chat-compact { + --transcript-max-h: clamp(66px, 11dvh, 100px); + } + + body[data-device-mode="tablet"].workspace-chat-compact #chat { + flex: 0 0 128px; + height: 128px; + max-height: 128px; + } + + body[data-device-mode="tablet"].workspace-chat-compact #ts, + body[data-device-mode="tablet"].workspace-chat-compact #sugs { + display: none !important; + } + + body[data-device-mode="tablet"].workspace-chat-expanded { + --transcript-max-h: clamp(130px, 23dvh, 190px); + } + + body[data-device-mode="tablet"].workspace-chat-expanded #chat { + flex: 0 0 210px; + height: 210px; + max-height: 210px; + } + + body[data-device-mode="tablet"]:is(.workspace-chat-side-left, .workspace-chat-side-right) { + --transcript-max-h: clamp(90px, 16dvh, 130px); + } + + body[data-device-mode="tablet"]:is(.workspace-chat-side-left, .workspace-chat-side-right) #chat { + flex: 0 0 178px; + height: 178px; + max-height: 178px; + } + + body[data-device-mode="tablet"].workspace-chat-minimal #chat { + max-height: 66px; + } + + body[data-device-mode="tablet"].workspace-chat-minimal #chat-feed, + body[data-device-mode="tablet"].workspace-chat-minimal #ts, + body[data-device-mode="tablet"].workspace-chat-minimal #dv-voice-bar, + body[data-device-mode="tablet"].workspace-chat-minimal #sugs { + display: none !important; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-compact { + --transcript-max-h: 46px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-compact #chat { + flex: 0 0 118px; + height: 118px; + max-height: 118px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-compact #ts, + body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-compact #sugs { + display: none !important; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-expanded { + --transcript-max-h: clamp(72px, 13dvh, 112px); + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-expanded #chat { + flex: 0 0 172px; + height: 172px; + max-height: 172px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]):is(.workspace-chat-side-left, .workspace-chat-side-right) { + --transcript-max-h: clamp(58px, 11dvh, 86px); + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]):is(.workspace-chat-side-left, .workspace-chat-side-right) #chat { + flex: 0 0 150px; + height: 150px; + max-height: 150px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-minimal #chat { + max-height: 62px; + } + + body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-minimal #chat-feed, + body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-minimal #ts, + body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-minimal #dv-voice-bar, + body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-minimal #sugs { + display: none !important; + } + + /* A physically narrow browser must remain recoverable even if a wider + workspace mode was saved on another device. */ + @media (max-width: 600px) { + .top-device-switcher { + display: flex !important; + gap: 1px !important; + padding-inline: 1px !important; + margin-left: auto; + } + + .top-device-switcher .ctab { + display: inline-flex !important; + align-items: center; + justify-content: center; + width: 25px; + height: 25px; + padding: 0; + } + + #pp-pull-tab { + display: none !important; + } + + #bframe, + .dm-desktop #bframe, + .dm-tablet #bframe, + .dm-android #bframe, + .dm-mobile #bframe { + align-self: stretch; + width: calc(100% - 4px) !important; + max-width: calc(100% - 4px) !important; + margin: 2px !important; + } + } diff --git a/vps/daveai-project-catalog.json b/vps/daveai-project-catalog.json new file mode 100644 index 00000000000..cd2a8dcbe50 --- /dev/null +++ b/vps/daveai-project-catalog.json @@ -0,0 +1,61 @@ +{ + "generated": "2026-07-25T03:24:00Z", + "source": "DaveAI verified production route and project catalog", + "projects": [ + { "id": "srv-daveai-home", "name": "DaveAI Core", "url": "https://daveai.tech/", "cat": "web", "status": "live", "embed": true, "description": "Main DaveAI Core platform." }, + { "id": "srv-daveai-dev", "name": "DaveAI Dev", "url": "https://dev.daveai.tech/", "cat": "web", "status": "auth", "description": "Protected developer console with live DaveAI dependency probes." }, + { "id": "srv-daveai-staging", "name": "DaveAI Staging", "url": "https://staging.daveai.tech/", "cat": "web", "status": "auth", "description": "Protected release gate with route checks and deployed UI fingerprinting." }, + { "id": "srv-daveai-diy", "name": "DaveAI DIY", "url": "https://diy.daveai.tech/", "cat": "web", "status": "auth", "description": "Protected project scaffold generator with downloadable multi-file output." }, + { "id": "srv-webui", "name": "DaveAI Web UI", "url": "https://webui.daveai.tech/", "cat": "web", "status": "live", "description": "Hosted web UI." }, + { "id": "srv-hermes-webui", "name": "Hermes Web UI", "url": "https://hermes-webui.daveai.tech/", "cat": "web", "status": "live", "description": "Hermes web UI." }, + { "id": "srv-hermes", "name": "Hermes", "url": "https://hermes.daveai.tech/", "cat": "apps", "status": "auth", "description": "Hermes agent surface protected by the DaveAI authentication portal." }, + { "id": "srv-hermes3d", "name": "Hermes 3D", "url": "https://hermes3d.daveai.tech/", "cat": "apps", "status": "live", "description": "Interactive live view of Brain health, tool count, and four DaveAI agents." }, + { "id": "srv-voice", "name": "DaveAI Voice", "url": "https://voice.daveai.tech/", "cat": "apps", "status": "live", "description": "Voice/Azure speech surface." }, + { "id": "srv-openhands", "name": "OpenHands", "url": "https://openhands.daveai.tech/", "cat": "apps", "status": "live", "description": "OpenHands coding agent surface." }, + { "id": "srv-fleet", "name": "DaveAI Fleet", "url": "https://fleet.daveai.tech/", "cat": "apps", "status": "auth", "description": "Protected redacted inventory of live PM2 processes and Docker containers." }, + { "id": "srv-auth", "name": "DaveAI Auth", "url": "https://auth.daveai.tech/", "cat": "apps", "status": "live", "description": "Live Authelia/auth gateway surface. Launch card only; cross-origin frame policy blocks embedding." }, + { "id": "srv-bolt", "name": "Bolt DIY", "url": "https://bolt.daveai.tech/", "cat": "apps", "status": "auth", "description": "Bolt.diy coding surface protected by DaveAI authentication. Launch card only." }, + { "id": "srv-hermestv", "name": "HermesTV", "url": "https://hermestv.daveai.tech/", "cat": "apps", "status": "live", "description": "Verified live HermesTV web surface." }, + { "id": "srv-tv", "name": "DaveAI TV", "url": "https://tv.daveai.tech/", "cat": "apps", "status": "live", "description": "Verified live TV web surface." }, + { "id": "srv-studio", "name": "DaveAI Studio", "url": "https://daveai.tech/studio/index.html", "cat": "web", "status": "needs-proof", "embed": true, "description": "Static studio page hosted under the DaveAI web root." }, + { "id": "srv-web-pages", "name": "DaveAI Web Pages", "url": "https://daveai.tech/web-pages.html", "cat": "web", "status": "needs-proof", "embed": true, "description": "Hosted web pages index discovered in the DaveAI web root." }, + { "id": "srv-windsurf", "name": "Windsurf", "url": "https://daveai.tech/windsurf.html", "cat": "web", "status": "needs-proof", "embed": true, "description": "Hosted Windsurf page discovered in the DaveAI web root." }, + { "id": "srv-voice-landing", "name": "Voice Landing", "url": "https://daveai.tech/voice-landing.html", "cat": "web", "status": "needs-proof", "embed": true, "description": "Voice landing page hosted under the DaveAI web root." }, + { "id": "srv-landing-v1", "name": "Landing V1", "url": "https://daveai.tech/index-landing-v1.html", "cat": "web", "status": "historical", "embed": true, "description": "Historical landing-page variant kept launchable for review." }, + { "id": "srv-games-portal", "name": "DaveAI Game Portal", "url": "https://game.daveai.tech/", "cat": "games", "status": "auth", "description": "Game portal subdomain protected by the DaveAI authentication portal." }, + { "id": "srv-siege-td", "name": "Daves Siege TD", "url": "https://daveai.tech/games/daves-siege-td/index.html", "cat": "games", "status": "live", "embed": true, "description": "Hosted tower-defense game." }, + { "id": "srv-td2", "name": "DaveAI TD2", "url": "https://daveai.tech/games/daveai-td2/index.html", "cat": "games", "status": "live", "embed": true, "description": "Hosted tower-defense game." }, + { "id": "srv-asteroids", "name": "DaveAI Asteroids", "url": "https://daveai.tech/games/asteroids/index.html", "cat": "games", "status": "live", "embed": true, "description": "Hosted arcade game." }, + { "id": "srv-arcade", "name": "DaveAI Arcade", "url": "https://daveai.tech/arcade/index.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Arcade hub discovered from /var/www/agentic-website/arcade." }, + { "id": "srv-arcade-snake", "name": "Arcade Snake", "url": "https://daveai.tech/arcade/snake.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Static arcade Snake game." }, + { "id": "srv-arcade-pacman", "name": "Arcade Pacman", "url": "https://daveai.tech/arcade/pacman.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Static arcade Pacman game." }, + { "id": "srv-arcade-tetris", "name": "Arcade Tetris", "url": "https://daveai.tech/arcade/tetris.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Static arcade Tetris game." }, + { "id": "srv-arcade-towerdefense", "name": "Arcade Tower Defense", "url": "https://daveai.tech/arcade/towerdefense.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Static arcade tower-defense game." }, + { "id": "srv-game-snake", "name": "Snake", "url": "https://daveai.tech/snake.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Snake game." }, + { "id": "srv-game-pacman", "name": "Pacman", "url": "https://daveai.tech/pacman.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Pacman game." }, + { "id": "srv-game-tictactoe", "name": "Tic Tac Toe", "url": "https://daveai.tech/tictactoe.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Tic Tac Toe game." }, + { "id": "srv-game-tictactoe-ai", "name": "AI Tic Tac Toe", "url": "https://daveai.tech/tic-tac-toe-ai.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "AI Tic Tac Toe page generated by DaveAI agents." }, + { "id": "srv-game-chess", "name": "Chess", "url": "https://daveai.tech/chess.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Chess page." }, + { "id": "srv-game-checkers", "name": "Checkers", "url": "https://daveai.tech/checkers.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Checkers page." }, + { "id": "srv-game-breakout", "name": "Breakout", "url": "https://daveai.tech/breakout.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Breakout game." }, + { "id": "srv-game-memory", "name": "Memory", "url": "https://daveai.tech/memory.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Memory game." }, + { "id": "srv-game-wheel-hangman", "name": "Wheel Hangman", "url": "https://daveai.tech/wheel-hangman.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Wheel/Hangman game page. Needs gameplay proof before accepted." }, + { "id": "srv-game-checkers-crowning", "name": "Checkers Crowning Draft", "url": "https://daveai.tech/checkers-game-crowning-jumping-do-not.html", "cat": "games", "status": "needs-fix", "embed": true, "description": "Agent-created page with mismatched checkers/hangman history; kept visible but not accepted." }, + { "id": "srv-game-gangster-wars", "name": "Gangster Wars", "url": "https://game.daveai.tech/gangster-wars/", "cat": "games", "status": "auth", "description": "Game subdomain project protected by the DaveAI authentication portal." }, + { "id": "srv-streamhub", "name": "StreamHub IPTV", "url": "https://iptv.daveai.tech/", "cat": "apps", "status": "partial-accepted", "description": "StreamHub IPTV app. Provider-vault catalogs, playback, library and playlists are partially accepted; broader voice/audio/provider sweep is pending." }, + { "id": "srv-apps-launcher", "name": "Apps DaveAI Launcher", "url": "https://apps.daveai.tech/", "cat": "apps", "status": "live", "description": "DaveTV/Apps launcher and authentication surface." }, + { "id": "srv-iptv-zero", "name": "IPTV Player Zero", "url": "https://apps.daveai.tech/iptv-player-zero/", "cat": "apps", "status": "provider-ready", "description": "Apps fleet IPTV player. Needs app-specific proof before E2E accepted." }, + { "id": "srv-xstream", "name": "XStream Player", "url": "https://xstream-player.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault XStream player." }, + { "id": "srv-smart-iptv", "name": "Smart IPTV Web", "url": "https://smart-iptv-web.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Smart IPTV Web player." }, + { "id": "srv-iptvnator", "name": "IPTVnator", "url": "https://iptvnator.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "IPTVnator provider-vault player." }, + { "id": "srv-stalker", "name": "Stalker UI", "url": "https://stalker-ui.daveai.tech/", "cat": "apps", "status": "needs-proof", "description": "Stalker UI. Cards/playback proof is still pending." }, + { "id": "srv-extreme-infinitv", "name": "Extreme InfiniTV", "url": "https://extreme-infinitv.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault IPTV app." }, + { "id": "srv-nuvio", "name": "NuvioWeb", "url": "https://nuvio.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault Stremio-style app." }, + { "id": "srv-wizju", "name": "Wizju IPTV Player", "url": "https://wizju-iptv-player.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault IPTV app." }, + { "id": "srv-iptv-stream", "name": "IPTV Stream", "url": "https://iptv-stream.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault live TV app." }, + { "id": "srv-iptv-restream", "name": "IPTV Restream", "url": "https://iptv-restream.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault restream app." }, + { "id": "srv-open-tv", "name": "Open TV", "url": "https://open-tv.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault TV app." }, + { "id": "srv-ynotv", "name": "ynotv", "url": "https://ynotv.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault TV app." }, + { "id": "srv-tvapp", "name": "TVapp", "url": "https://tvapp.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault TV app." } + ] +} diff --git a/vps/daveai-sites-config.json b/vps/daveai-sites-config.json new file mode 100644 index 00000000000..b4f2d9df3fa --- /dev/null +++ b/vps/daveai-sites-config.json @@ -0,0 +1,257 @@ +{ + "sites": [ + { + "id": "daveai-main", + "name": "DaveAI", + "domain": "daveai.tech", + "url": "https://daveai.tech", + "port": 3001, + "status": "live", + "category": "core", + "icon": "🌐", + "description": "Main DaveAI website" + }, + { + "id": "daveai-auth", + "name": "Auth", + "domain": "auth.daveai.tech", + "url": "https://auth.daveai.tech", + "port": 8888, + "status": "live", + "category": "core", + "icon": "🔐", + "description": "Authelia SSO authentication" + }, + { + "id": "daveai-api", + "name": "API", + "domain": "api.daveai.tech", + "url": "https://api.daveai.tech/health", + "port": 8888, + "status": "live", + "category": "core", + "icon": "⚡", + "description": "Agent Brain API gateway" + }, + { + "id": "daveai-voice", + "name": "Voice", + "domain": "voice.daveai.tech", + "url": "https://voice.daveai.tech", + "port": 5050, + "status": "live", + "category": "ai", + "icon": "🎤", + "description": "Edge-TTS voice synthesis" + }, + { + "id": "daveai-ai", + "name": "AI", + "domain": "ai.daveai.tech", + "url": "https://ai.daveai.tech", + "port": 4000, + "status": "live", + "category": "ai", + "icon": "🤖", + "description": "LiteLLM proxy for all models" + }, + { + "id": "daveai-llm", + "name": "LLM", + "domain": "llm.daveai.tech", + "url": "https://llm.daveai.tech", + "port": 4000, + "status": "live", + "category": "ai", + "icon": "🧠", + "description": "LLM model serving endpoint" + }, + { + "id": "daveai-openhands", + "name": "OpenHands", + "domain": "openhands.daveai.tech", + "url": "https://openhands.daveai.tech", + "port": 3333, + "status": "live", + "category": "ai", + "icon": "🖐️", + "description": "AI agent IDE" + }, + { + "id": "daveai-diy", + "name": "Bolt.DIY", + "domain": "diy.daveai.tech", + "url": "https://diy.daveai.tech", + "port": 3300, + "status": "auth", + "category": "dev", + "icon": "🔧", + "description": "Protected project scaffold generator" + }, + { + "id": "daveai-dev", + "name": "Dev", + "domain": "dev.daveai.tech", + "url": "https://dev.daveai.tech", + "port": 3398, + "status": "auth", + "category": "dev", + "icon": "💻", + "description": "Protected live dependency-health console" + }, + { + "id": "daveai-staging", + "name": "Staging", + "domain": "staging.daveai.tech", + "url": "https://staging.daveai.tech", + "port": 3399, + "status": "auth", + "category": "dev", + "icon": "🧪", + "description": "Protected production release-readiness gate" + }, + { + "id": "daveai-docs", + "name": "Docs", + "domain": "docs.daveai.tech", + "url": "https://docs.daveai.tech", + "port": 3100, + "status": "live", + "category": "docs", + "icon": "📚", + "description": "Documentation site" + }, + { + "id": "daveai-git", + "name": "Git", + "domain": "git.daveai.tech", + "url": "https://git.daveai.tech", + "port": 3301, + "status": "auth", + "category": "dev", + "icon": "📦", + "description": "Protected persistent Gitea repository service" + }, + { + "id": "daveai-db", + "name": "Database", + "domain": "db.daveai.tech", + "url": "https://db.daveai.tech", + "port": 8080, + "status": "auth", + "category": "infra", + "icon": "🗄️", + "description": "Protected Adminer console for the Bolt PostgreSQL service" + }, + { + "id": "daveai-monitor", + "name": "Monitor", + "domain": "monitor.daveai.tech", + "url": "https://monitor.daveai.tech", + "port": 3030, + "status": "auth", + "category": "infra", + "icon": "📊", + "description": "Protected Grafana dashboard backed by Prometheus host metrics" + }, + { + "id": "daveai-hermestv", + "name": "HermesTV", + "domain": "hermestv.daveai.tech", + "url": "https://hermestv.daveai.tech", + "port": 3080, + "status": "live", + "category": "media", + "icon": "📺", + "description": "TV streaming interface" + }, + { + "id": "daveai-iptv", + "name": "IPTV", + "domain": "iptv.daveai.tech", + "url": "https://iptv.daveai.tech", + "port": 3103, + "status": "live", + "category": "media", + "icon": "📡", + "description": "IPTV restreaming" + }, + { + "id": "daveai-hermes", + "name": "Hermes", + "domain": "hermes-webui.daveai.tech", + "url": "https://hermes-webui.daveai.tech", + "port": 8787, + "status": "live", + "category": "bots", + "icon": "🤖", + "description": "Discord bots WebUI" + }, + { + "id": "daveai-apps", + "name": "Apps", + "domain": "apps.daveai.tech", + "url": "https://apps.daveai.tech", + "port": 3200, + "status": "live", + "category": "apps", + "icon": "📱", + "description": "Apps portal (DaveTV)" + }, + { + "id": "daveai-fleet", + "name": "Fleet", + "domain": "fleet.daveai.tech", + "url": "https://fleet.daveai.tech", + "port": 3400, + "status": "auth", + "category": "infra", + "icon": "🚀", + "description": "Protected redacted PM2 and Docker runtime inventory" + }, + { + "id": "daveai-ws", + "name": "WebSocket", + "domain": "ws.daveai.tech", + "url": "https://ws.daveai.tech", + "port": 3401, + "status": "live", + "category": "infra", + "icon": "🔌", + "description": "Public secure WebSocket echo and test console" + }, + { + "id": "daveai-game", + "name": "Game", + "domain": "game.daveai.tech", + "url": "https://game.daveai.tech", + "port": 3500, + "status": "auth", + "category": "game", + "icon": "🎮", + "description": "Game server (coming soon)" + }, + { + "id": "daveai-3d", + "name": "3D", + "domain": "hermes3d.daveai.tech", + "url": "https://hermes3d.daveai.tech", + "port": 3600, + "status": "live", + "category": "apps", + "icon": "🧊", + "description": "Interactive live DaveAI Brain and agent graph" + } + ], + "categories": { + "core": { "name": "Core", "color": "#00ff88" }, + "ai": { "name": "AI/ML", "color": "#ff6b6b" }, + "dev": { "name": "Development", "color": "#4ecdc4" }, + "infra": { "name": "Infrastructure", "color": "#ffe66d" }, + "media": { "name": "Media", "color": "#ff9f43" }, + "bots": { "name": "Bots", "color": "#a29bfe" }, + "apps": { "name": "Apps", "color": "#fd79a8" }, + "game": { "name": "Gaming", "color": "#e17055" }, + "future": { "name": "Future", "color": "#b2bec3" } + } +} diff --git a/vps/daveai-ui-v6.html b/vps/daveai-ui-v6.html new file mode 100644 index 00000000000..bb23f5bf9c4 --- /dev/null +++ b/vps/daveai-ui-v6.html @@ -0,0 +1,12920 @@ + + + + + + + DaveAI — Agentic Web Builder + + + + + + + + + +
+
+
+
+ + + + + + +
+ +
+ +
+ + + + +
+ + + + + +
+ + + 100% + + +
+ + + + + +
+ + +
+ + Chat + + +
+ + +
+ +
+
Welcome to DaveAI
+
Choose your device to enter
+
+
+ +
Desktop
+
1280px+
+
RECOMMENDED
+
+
+ +
Tablet
+
768px
+
RECOMMENDED
+
+
+ +
Mobile
+
412px
+
RECOMMENDED
+
+
+
+ + + + +
+ + +
+
+
+ Settings + +
+ +
+
Intro Video
+
Choose which intro plays when you load or refresh the page. New videos can be added over + time.
+
+ + + + +
+
Saved
+
+ +
+
Intro Volume
+
+ + 60% +
+
+ +
+
Display
+
+
+ Show file extensions + Display .html, .zip, etc. in project & file names +
+ + Admin +
+
Saved
+ +
+
+ Auto-immersive on project load + Maximize preview when opening a project (game, app, web, other) +
+ +
+
Saved
+ +
+
+ Edge-hover panels + Hover far left/right/top/bottom edges to reveal hidden UI in immersive mode +
+ +
+
Saved
+ +
+
+ Show Thinking… + Show DaveAI's internal step events (e.g. 💭 Thinking…) inside the chat + bubble. Off by default — activity panel always logs them. +
+ +
+
Saved +
+ +
+
+ Showcase Demo + Show featured project (game/app) in the preview window on load. Auto-minimizes when you start + typing. +
+ +
+
Saved +
+
+ +
+
+
+ DaveAI Voice + Enable voice narration — agents speak their responses aloud +
+ +
+
+ +
+
+ +
+
Demo / Showcase Admin +
+
Set a project to showcase in the Action Window by default. Visitors see the demo with + Play/Create buttons.
+
+ + + +
+ +
+ +
+ +
+
+ + +
+
+
Saved
+
+ + +
+
+ Waveform & Colors + Customize voice waveform style and colors for AI (female) and Human (male) voices +
+ +
+ +
+
+ + Custom color picker +
+
+ +
+ +
+
+ + Custom color picker +
+
+ +
+ +
+
+
+ + +
+
+ Visual Effects & Transitions + Enable hover effects, particles, flames, and other visual artifacts on UI elements +
+ +
+
+ Hover Lift & Glow + Buttons, cards, and panels lift with glow on hover +
+ +
+ +
+
+ Card Shimmer + Light shimmer sweeps across cards and panels on hover +
+ +
+ +
+
+ Click Particles + Burst of particles when clicking buttons and interactive elements +
+ +
+ +
+
+ Flame Effects + Animated flames on active/highlighted elements (performance-intensive) +
+ +
+ +
+
+ Static / Glitch + CRT static and glitch overlay on hover — retro AI aesthetic +
+ +
+ +
+
+ Neon Text Glow + Labels and headings glow with neon effect on hover +
+ +
+ +
+
+ Panel Transitions + Smooth slide/fade/scale animations when panels open/close +
+ +
+ +
+
+ Electric Spark Borders + Rotating conic gradient spark around focused elements +
+ +
+
+ + +
+
+ Icon & Theme Studio + Load icon packs, change themes. Auto-backup every 10 min. Safe mode if icons break. +
+ +
+
+
+
Auto-backup: Active
+
Last: never
+
+ + +
+ +
+
+ Safe Mode + If an icon pack causes issues, auto-revert to stock icons within 30 seconds +
+ +
+ +
+
+ Auto-Backup (10 min) + Automatically save working theme/icon state every 10 minutes +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+ DaveAI Personality & Memory + Configure how DaveAI talks, remembers, and entertains +
+ +
+ + + + +
+ + 70% +
+ +
+
+ 18+ Jokes Allowed + Include adult humor when appropriate (admin-controlled) +
+ +
+ +
+
+ Long Stories & Poems + DaveAI can tell long stories, recite poems, or go on entertaining tangents +
+ +
+ +
+
+ Conversation Memory + Remember past conversations, preferences, and context across sessions +
+ +
+ + +
+ + 100 +
+ +
+ + + +
+ + + +
+
+ + +
+
+ Auto-Refresh Projects + Periodically scan for new, updated, or changed projects across all categories +
+
+
+ Enable Auto-Refresh + Checks every 2 hours for changes in games, apps, web, and other categories +
+ +
+
+ + + + +
+
+ + +
+
+ Local LLM (LM Studio) + Admin + Use your local RTX 3090 Ti to power DaveAI stories, voice, and chat via LM Studio +
+ +
+
+ Enable Local LLM + Route DaveAI chat through LM Studio instead of cloud API +
+ +
+ +
+
+ Auto-load LM Studio Models + Automatically select best available model (14B+ with 80k+ context) +
+ +
+ +
+ + + + + + + + + Automatically swap between story and coding + models. Uses MOE for instant loads. + +
+
+ Enable Auto-Switch + Load story model for chat, coding model when idle +
+ +
+ + + + + +
+ + 2 min +
+ + + + + + + +
+
+ DaveAI voice output is locked to Azure English Neural voices. Use Voice Studio to select Maisie or another + approved Azure voice. +
+ + Azure Neural only +
+ + +
+ + 0.8 +
+ + +
+ + 2048 +
+ +
+ + +
+
Not connected
+
+
+ +
+
+ + + +
+
+ DaveAI Chat + +
+
+
+ + +
+
+ + + + +
+
+ +
+ + +
+
+
+ +
+
+ + +
Don't have an account? Sign Up
+
+
+
+ + + + + +
+
+
+ tool_name + Coder + +
+
Waiting for output...
+
+ + +
+
+
+ + +
+ + +
+ + + +
+
+ + + + +
+
+ +
+ + + + +
+
+
+ + + + +
+
+
+
+
+
connecting +
+ +
Horizons
+
+ + + + +
+
+
+ + + +
+
+ + LIVE + ◌ ... +
+
+
+ connecting…
+
+ + +
+ + + + + +
+ +
+ + +
+
+
+

Pages

+ +
+
+ + + + + + + +
+ + + + + +
+
+
Loading deployed pages…
+
+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ +
+ + +
+
Desktop
+
Tablet
+
Android
+
Mobile
+ +
daveai.tech
+
+ + + +
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+
+
daveai.tech
+
+
+ +
+
+ + Waiting for build — start a conversation to preview your project +
+
+ +
+
Game
+

Dave's Siege TD

+

Experience our latest tower defense game — built entirely by DaveAI + agents. Defend your castle with strategic tower placement!

+
+ + +
+
+ by DaveAI Live Games +
+ + +
+ + + + + +
+ Playing + + + + +
+ + +
+ + DaveAI speaking… +
+ + + + +
+ +
+ + +
+
+ Build Timeline + +
+
+
Loading build history…
+
+
+ + +
+
Analytics & Health
+
+
+
Total Pages
+
5
+
2 published
+
+
+
Recent Builds
+
12
+
total logged
+
+
+
Commits
+
34
+
this week
+
+
+
API Status
+
+
checking
+
+
+
Events
+
1.2k
+
tracked
+
+
+
Registered Users
+
84k
+
accounts
+
+
+
+ + Health API + + Agents Status + Pages API + Build + Status +
+
+ + +
+
Admin
+
+ + + + + +
+
+
Brain URLhttps://daveai.tech/api
+
LiteLLM:4000 (local)
+
ZeroClaw:3000 (gateway)
+
DBPostgreSQL (daveai)
+
ModelLiteLLM routed
+
Toolsloading…
+
VPS187.77.30.206
+
+ + + + +
+ + + +
+
+
+

Chat Mode

+
DaveAI is ready in the dock below. Replies, memory, voice, and agent activity stay + live while this action surface remains focused on the conversation.
+
    +
  • Clean final replies in chat
  • +
  • Activity steps stay in the right panel
  • +
  • Azure Neural voice reads final answers only
  • +
+
+ + +
+
Prepares the chat composer without sending.
+
+
+
+ + +
+
+
+

Browser Mode

+
Browser Mode sends pages to DaveAI's web tools for reading, + screenshots, and step-by-step follow-up inside this action window.
+
+ + +
+
    +
  • Browse, read, and summarize any public page
  • +
  • Click + form-fill requests route through DaveAI's approved web tools
  • +
  • Capture screenshots into right-panel Activity
  • +
+ View current roadmap +
+
+
+ + +
+
+
+

Code Agent Mode

+
Code Agent mode keeps plans, file review, test output, + and deploy notes organized while DaveAI works.
+
    +
  • Plan + assumptions panel (left)
  • +
  • File tree with inline diff (center)
  • +
  • Test output + deploy chip (right)
  • +
+ View current state +
+
+
+ + +
+
+
+

Research Mode

+
Research Agent mode keeps sources, citations, + and reading notes organized while DaveAI investigates.
+
+ + + + +
+
The prepared request stays editable in chat before sending.
+
    +
  • Choose live-web, primary-doc, or workspace evidence
  • +
  • Choose the citation detail before sending
  • +
  • Sources and tool activity remain visible in the right panel
  • +
+ View proof index +
+
+
+ + +
+
+
+

Problem Solver Mode

+
Problem Solver mode turns the request into assumptions, + a step-by-step plan, and test results as DaveAI works.
+
+ + +
+
Builds a diagnosis request with assumptions and proof requirements.
+
    +
  • Problem statement and assumption list
  • +
  • Step-by-step plan with test results
  • +
  • Re-run tests on demand
  • +
+ View completion backlog +
+
+
+ + +
+
+
+

Hermes Team Mode

+
Hermes Team mode shows delegated work, worker status, + and approval gates from DaveAI's Hermes agents.
+
+ + +
+
Current Brain status is shown in the global status bar; delegated writes remain approval-gated.
+
    +
  • Supervisor routes work to the appropriate DaveAI agents
  • +
  • Agent state and traces appear in Activity
  • +
  • Approval gates remain attached to delegated writes
  • +
+ View file matrix +
+
+
+ + +
+
+
+
Select a saved project
+ + + + +
+
+
+
Loading tree…
+
+
+
Select a project file to edit.
+
+
+
+
+ +
+ + +
+ +
+
+ DaveAI Chat +
+ + + +
+
+ +
+
Supervisor: I'm having @Coder update the hero section. @QA will + verify contrast ratios.
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+
Maisie
+
NEURAL
+
+
+
+ +
+ +
+ +
+ + + + + + type /voice to switch +
+
+
+ +
+
hero-title
+ + + + +
+ +
+
+
Make button purple gradient
+
Add dark mode toggle
+
Check mobile layout
+
Generate logo
+
Add pricing section
+
Run lighthouse audit
+
Screenshot the site
+
+
+
+ + + + +
+ + + + + + + + + + diff --git a/vps/patches/agent-runtime-completion-20260724-src/daveai-ui-v6.html b/vps/patches/agent-runtime-completion-20260724-src/daveai-ui-v6.html new file mode 100644 index 00000000000..bb23f5bf9c4 --- /dev/null +++ b/vps/patches/agent-runtime-completion-20260724-src/daveai-ui-v6.html @@ -0,0 +1,12920 @@ + + + + + + + DaveAI — Agentic Web Builder + + + + + + + + + +
+
+
+
+ + + + + + +
+ +
+ +
+ + + + +
+ + + + + +
+ + + 100% + + +
+ + + + + +
+ + +
+ + Chat + + +
+ + +
+ +
+
Welcome to DaveAI
+
Choose your device to enter
+
+
+ +
Desktop
+
1280px+
+
RECOMMENDED
+
+
+ +
Tablet
+
768px
+
RECOMMENDED
+
+
+ +
Mobile
+
412px
+
RECOMMENDED
+
+
+
+ + + + +
+ + +
+
+
+ Settings + +
+ +
+
Intro Video
+
Choose which intro plays when you load or refresh the page. New videos can be added over + time.
+
+ + + + +
+
Saved
+
+ +
+
Intro Volume
+
+ + 60% +
+
+ +
+
Display
+
+
+ Show file extensions + Display .html, .zip, etc. in project & file names +
+ + Admin +
+
Saved
+ +
+
+ Auto-immersive on project load + Maximize preview when opening a project (game, app, web, other) +
+ +
+
Saved
+ +
+
+ Edge-hover panels + Hover far left/right/top/bottom edges to reveal hidden UI in immersive mode +
+ +
+
Saved
+ +
+
+ Show Thinking… + Show DaveAI's internal step events (e.g. 💭 Thinking…) inside the chat + bubble. Off by default — activity panel always logs them. +
+ +
+
Saved +
+ +
+
+ Showcase Demo + Show featured project (game/app) in the preview window on load. Auto-minimizes when you start + typing. +
+ +
+
Saved +
+
+ +
+
+
+ DaveAI Voice + Enable voice narration — agents speak their responses aloud +
+ +
+
+ +
+
+ +
+
Demo / Showcase Admin +
+
Set a project to showcase in the Action Window by default. Visitors see the demo with + Play/Create buttons.
+
+ + + +
+ +
+ +
+ +
+
+ + +
+
+
Saved
+
+ + +
+
+ Waveform & Colors + Customize voice waveform style and colors for AI (female) and Human (male) voices +
+ +
+ +
+
+ + Custom color picker +
+
+ +
+ +
+
+ + Custom color picker +
+
+ +
+ +
+
+
+ + +
+
+ Visual Effects & Transitions + Enable hover effects, particles, flames, and other visual artifacts on UI elements +
+ +
+
+ Hover Lift & Glow + Buttons, cards, and panels lift with glow on hover +
+ +
+ +
+
+ Card Shimmer + Light shimmer sweeps across cards and panels on hover +
+ +
+ +
+
+ Click Particles + Burst of particles when clicking buttons and interactive elements +
+ +
+ +
+
+ Flame Effects + Animated flames on active/highlighted elements (performance-intensive) +
+ +
+ +
+
+ Static / Glitch + CRT static and glitch overlay on hover — retro AI aesthetic +
+ +
+ +
+
+ Neon Text Glow + Labels and headings glow with neon effect on hover +
+ +
+ +
+
+ Panel Transitions + Smooth slide/fade/scale animations when panels open/close +
+ +
+ +
+
+ Electric Spark Borders + Rotating conic gradient spark around focused elements +
+ +
+
+ + +
+
+ Icon & Theme Studio + Load icon packs, change themes. Auto-backup every 10 min. Safe mode if icons break. +
+ +
+
+
+
Auto-backup: Active
+
Last: never
+
+ + +
+ +
+
+ Safe Mode + If an icon pack causes issues, auto-revert to stock icons within 30 seconds +
+ +
+ +
+
+ Auto-Backup (10 min) + Automatically save working theme/icon state every 10 minutes +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+ DaveAI Personality & Memory + Configure how DaveAI talks, remembers, and entertains +
+ +
+ + + + +
+ + 70% +
+ +
+
+ 18+ Jokes Allowed + Include adult humor when appropriate (admin-controlled) +
+ +
+ +
+
+ Long Stories & Poems + DaveAI can tell long stories, recite poems, or go on entertaining tangents +
+ +
+ +
+
+ Conversation Memory + Remember past conversations, preferences, and context across sessions +
+ +
+ + +
+ + 100 +
+ +
+ + + +
+ + + +
+
+ + +
+
+ Auto-Refresh Projects + Periodically scan for new, updated, or changed projects across all categories +
+
+
+ Enable Auto-Refresh + Checks every 2 hours for changes in games, apps, web, and other categories +
+ +
+
+ + + + +
+
+ + +
+
+ Local LLM (LM Studio) + Admin + Use your local RTX 3090 Ti to power DaveAI stories, voice, and chat via LM Studio +
+ +
+
+ Enable Local LLM + Route DaveAI chat through LM Studio instead of cloud API +
+ +
+ +
+
+ Auto-load LM Studio Models + Automatically select best available model (14B+ with 80k+ context) +
+ +
+ +
+ + + + + + + + + Automatically swap between story and coding + models. Uses MOE for instant loads. + +
+
+ Enable Auto-Switch + Load story model for chat, coding model when idle +
+ +
+ + + + + +
+ + 2 min +
+ + + + + + + +
+
+ DaveAI voice output is locked to Azure English Neural voices. Use Voice Studio to select Maisie or another + approved Azure voice. +
+ + Azure Neural only +
+ + +
+ + 0.8 +
+ + +
+ + 2048 +
+ +
+ + +
+
Not connected
+
+
+ +
+
+ + + +
+
+ DaveAI Chat + +
+
+
+ + +
+
+ + + + +
+
+ +
+ + +
+
+
+ +
+
+ + +
Don't have an account? Sign Up
+
+
+
+ + + + + +
+
+
+ tool_name + Coder + +
+
Waiting for output...
+
+ + +
+
+
+ + +
+ + +
+ + + +
+
+ + + + +
+
+ +
+ + + + +
+
+
+ + + + +
+
+
+
+
+
connecting +
+ +
Horizons
+
+ + + + +
+
+
+ + + +
+
+ + LIVE + ◌ ... +
+
+
+ connecting…
+
+ + +
+ + + + + +
+ +
+ + +
+
+
+

Pages

+ +
+
+ + + + + + + +
+ + + + + +
+
+
Loading deployed pages…
+
+ +
+ + + + + + + + + + + + + + + + +
+ + +
+ +
+ + +
+
Desktop
+
Tablet
+
Android
+
Mobile
+ +
daveai.tech
+
+ + + +
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+
+
daveai.tech
+
+
+ +
+
+ + Waiting for build — start a conversation to preview your project +
+
+ +
+
Game
+

Dave's Siege TD

+

Experience our latest tower defense game — built entirely by DaveAI + agents. Defend your castle with strategic tower placement!

+
+ + +
+
+ by DaveAI Live Games +
+ + +
+ + + + + +
+ Playing + + + + +
+ + +
+ + DaveAI speaking… +
+ + + + +
+ +
+ + +
+
+ Build Timeline + +
+
+
Loading build history…
+
+
+ + +
+
Analytics & Health
+
+
+
Total Pages
+
5
+
2 published
+
+
+
Recent Builds
+
12
+
total logged
+
+
+
Commits
+
34
+
this week
+
+
+
API Status
+
+
checking
+
+
+
Events
+
1.2k
+
tracked
+
+
+
Registered Users
+
84k
+
accounts
+
+
+
+ + Health API + + Agents Status + Pages API + Build + Status +
+
+ + +
+
Admin
+
+ + + + + +
+
+
Brain URLhttps://daveai.tech/api
+
LiteLLM:4000 (local)
+
ZeroClaw:3000 (gateway)
+
DBPostgreSQL (daveai)
+
ModelLiteLLM routed
+
Toolsloading…
+
VPS187.77.30.206
+
+ + + + +
+ + + +
+
+
+

Chat Mode

+
DaveAI is ready in the dock below. Replies, memory, voice, and agent activity stay + live while this action surface remains focused on the conversation.
+
    +
  • Clean final replies in chat
  • +
  • Activity steps stay in the right panel
  • +
  • Azure Neural voice reads final answers only
  • +
+
+ + +
+
Prepares the chat composer without sending.
+
+
+
+ + +
+
+
+

Browser Mode

+
Browser Mode sends pages to DaveAI's web tools for reading, + screenshots, and step-by-step follow-up inside this action window.
+
+ + +
+
    +
  • Browse, read, and summarize any public page
  • +
  • Click + form-fill requests route through DaveAI's approved web tools
  • +
  • Capture screenshots into right-panel Activity
  • +
+ View current roadmap +
+
+
+ + +
+
+
+

Code Agent Mode

+
Code Agent mode keeps plans, file review, test output, + and deploy notes organized while DaveAI works.
+
    +
  • Plan + assumptions panel (left)
  • +
  • File tree with inline diff (center)
  • +
  • Test output + deploy chip (right)
  • +
+ View current state +
+
+
+ + +
+
+
+

Research Mode

+
Research Agent mode keeps sources, citations, + and reading notes organized while DaveAI investigates.
+
+ + + + +
+
The prepared request stays editable in chat before sending.
+
    +
  • Choose live-web, primary-doc, or workspace evidence
  • +
  • Choose the citation detail before sending
  • +
  • Sources and tool activity remain visible in the right panel
  • +
+ View proof index +
+
+
+ + +
+
+
+

Problem Solver Mode

+
Problem Solver mode turns the request into assumptions, + a step-by-step plan, and test results as DaveAI works.
+
+ + +
+
Builds a diagnosis request with assumptions and proof requirements.
+
    +
  • Problem statement and assumption list
  • +
  • Step-by-step plan with test results
  • +
  • Re-run tests on demand
  • +
+ View completion backlog +
+
+
+ + +
+
+
+

Hermes Team Mode

+
Hermes Team mode shows delegated work, worker status, + and approval gates from DaveAI's Hermes agents.
+
+ + +
+
Current Brain status is shown in the global status bar; delegated writes remain approval-gated.
+
    +
  • Supervisor routes work to the appropriate DaveAI agents
  • +
  • Agent state and traces appear in Activity
  • +
  • Approval gates remain attached to delegated writes
  • +
+ View file matrix +
+
+
+ + +
+
+
+
Select a saved project
+ + + + +
+
+
+
Loading tree…
+
+
+
Select a project file to edit.
+
+
+
+
+ +
+ + +
+ +
+
+ DaveAI Chat +
+ + + +
+
+ +
+
Supervisor: I'm having @Coder update the hero section. @QA will + verify contrast ratios.
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+
Maisie
+
NEURAL
+
+
+
+ +
+ +
+ +
+ + + + + + type /voice to switch +
+
+
+ +
+
hero-title
+ + + + +
+ +
+
+
Make button purple gradient
+
Add dark mode toggle
+
Check mobile layout
+
Generate logo
+
Add pricing section
+
Run lighthouse audit
+
Screenshot the site
+
+
+
+ + + + +
+ + + + + + + + + + diff --git a/vps/patches/daveai-production-runtime-20260731/README.txt b/vps/patches/daveai-production-runtime-20260731/README.txt new file mode 100644 index 00000000000..31775f0e2fa --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/README.txt @@ -0,0 +1,29 @@ +DaveAI production runtime patch bundle - 2026-07-31 + +Purpose +------- +This bundle persists the production-only fixes made during the DaveAI.tech E2E repair audit so they are no longer trapped as manual VPS edits. + +Contents +-------- +- brain_llm.py + Production backend brain file containing the native Ollama direct-chat/streaming repair used by the main chat path. + +- nginx/ + Production Nginx files after the API/login rate-limit, health endpoint, brain vhost, voice health, API health, and StreamHub provider-vault fixes. + +- windsurf.html + Production Windsurf/Ops page after adding the unauthenticated auth gate that prevents protected admin API polling. + +- td2/index-BEGIhVuy.js + Production TD2 bundle after adding the asset cache-bust query string. + +- td2/generated-assets-manifest.txt + List of generated TD2 placeholder VFX PNG and silent MP3 assets that were added to stop missing-resource failures. + +- apply-production-runtime-fixes.sh + Idempotent deployment helper. It backs up current production files, installs the persisted files, recreates the TD2 generated assets, validates Nginx, and reloads Nginx. + +Notes +----- +This bundle intentionally does not reboot the VPS, run apt upgrades, or restart unrelated processes. Those are maintenance-window operations. diff --git a/vps/patches/daveai-production-runtime-20260731/apply-production-runtime-fixes.sh b/vps/patches/daveai-production-runtime-20260731/apply-production-runtime-fixes.sh new file mode 100644 index 00000000000..2b840c2daaf --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/apply-production-runtime-fixes.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +STAMP="$(date -u +%Y%m%dT%H%M%SZ)" +BACKUP_DIR="/opt/daveai/backups/${STAMP}-source-controlled-runtime-restore" + +AGENT_BRAIN_DIR="/opt/agent-brain" +SITE_ROOT="/var/www/agentic-website" +TD2_ROOT="${SITE_ROOT}/games/daveai-td2" + +mkdir -p "${BACKUP_DIR}/nginx" "${BACKUP_DIR}/td2" + +backup_if_exists() { + local src="$1" + local dest="$2" + if [ -e "$src" ]; then + mkdir -p "$(dirname "$dest")" + cp -a "$src" "$dest" + fi +} + +install_file() { + local src="$1" + local dest="$2" + local mode="${3:-0644}" + mkdir -p "$(dirname "$dest")" + cp -a "$src" "$dest" + chmod "$mode" "$dest" +} + +echo "Backing up current production files to ${BACKUP_DIR}" +backup_if_exists "${AGENT_BRAIN_DIR}/brain_llm.py" "${BACKUP_DIR}/brain_llm.py" +backup_if_exists "${SITE_ROOT}/windsurf.html" "${BACKUP_DIR}/windsurf.html" +backup_if_exists "${TD2_ROOT}/assets/index-BEGIhVuy.js" "${BACKUP_DIR}/td2/index-BEGIhVuy.js" + +for name in daveai.tech iptv.daveai.tech voice.daveai.tech brain.daveai.tech stories.daveai.tech; do + backup_if_exists "/etc/nginx/sites-enabled/${name}" "${BACKUP_DIR}/nginx/${name}" +done +backup_if_exists "/etc/nginx/conf.d/00-kilo-bot-protection.conf" "${BACKUP_DIR}/nginx/00-kilo-bot-protection.conf" + +echo "Installing backend brain and production HTML" +install_file "${SCRIPT_DIR}/brain_llm.py" "${AGENT_BRAIN_DIR}/brain_llm.py" +install_file "${SCRIPT_DIR}/windsurf.html" "${SITE_ROOT}/windsurf.html" +install_file "${SCRIPT_DIR}/td2/index-BEGIhVuy.js" "${TD2_ROOT}/assets/index-BEGIhVuy.js" + +echo "Installing Nginx configuration" +for name in daveai.tech iptv.daveai.tech voice.daveai.tech brain.daveai.tech stories.daveai.tech; do + install_file "${SCRIPT_DIR}/nginx/${name}" "/etc/nginx/sites-enabled/${name}" +done +install_file "${SCRIPT_DIR}/nginx/00-kilo-bot-protection.conf" "/etc/nginx/conf.d/00-kilo-bot-protection.conf" + +echo "Recreating TD2 generated assets from manifest" +python3 - <<'PY' "${SCRIPT_DIR}/td2/generated-assets-manifest.txt" "${TD2_ROOT}" +from pathlib import Path +import base64 +import sys + +manifest = Path(sys.argv[1]) +td2_root = Path(sys.argv[2]) + +png_payload = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAFElEQVR42mP4TyJgGNUwqmH4agAAr639H23ooMoAAAAASUVORK5CYII=" +) +mp3_payload = base64.b64decode( + "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYwLjE2LjEwMAAAAAAAAAAAAAAA//tAwAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAFAAACvgBoaGhoaGhoaGhoaGhoaGhoaGhojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo60tLS0tLS0tLS0tLS0tLS0tLS0tNra2tra2tra2tra2tra2tra2tra//////////////////////////8AAAAATGF2YzYwLjMxAAAAAAAAAAAAAAAAJAMGAAAAAAAAAr4QurGFAAAAAAD/+xDEAAPAAAGkAAAAIAAANIAAAARMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7EMQpg8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sQxFMDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+xDEfIPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7EMSmA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" +) + +written = {"png": 0, "mp3": 0} +for raw in manifest.read_text(encoding="utf-8-sig").splitlines(): + rel = raw.strip() + if not rel: + continue + target = td2_root / rel + target.parent.mkdir(parents=True, exist_ok=True) + if rel.endswith(".png"): + target.write_bytes(png_payload) + written["png"] += 1 + elif rel.endswith(".mp3"): + target.write_bytes(mp3_payload) + written["mp3"] += 1 + else: + raise SystemExit(f"Unsupported generated TD2 asset: {rel}") + +print(written) +PY + +echo "Validating Python and Nginx syntax" +python3 -m py_compile "${AGENT_BRAIN_DIR}/brain_llm.py" +nginx -t + +echo "Reloading Nginx and restarting agent-brain" +systemctl reload nginx +if command -v pm2 >/dev/null 2>&1; then + pm2 restart agent-brain --update-env +fi + +echo "Done. Backup: ${BACKUP_DIR}" diff --git a/vps/patches/daveai-production-runtime-20260731/brain_llm.py b/vps/patches/daveai-production-runtime-20260731/brain_llm.py new file mode 100644 index 00000000000..7c48bc68295 --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/brain_llm.py @@ -0,0 +1,430 @@ +"""brain_llm.py — LLM abstraction: 4-stage fallback, streaming, JSON mode, caching. + +Stage chain (no cloud-provider dependency — local-first): + 1. LiteLLM streaming (model → heavy-coder / fast-agent / vision via /opt/litellm/config.yaml) + 2. LiteLLM non-stream (same model, no stream) + 3. LiteLLM fast-agent (if HEAVY failed, retry with FAST) + 4. Direct LM Studio (bypass LiteLLM proxy entirely — final safety net) + +No Anthropic SDK dependency. Removed 2026-05-25 per Dave's directive: "anthropic isn't +one of the api keys in the list to be usable". If a cloud provider key (Anthropic/MiniMax/ +SiliconFlow/OpenRouter) is rotated and re-enabled later, the LiteLLM config can route +brain aliases through it transparently — no code change required here. +""" +import os, json, re, time, hashlib, threading, urllib.request, urllib.error, sys +from brain_core import LLM_BASE, HEAVY, FAST, VISION +from brain_events import agent_set, emit + + +# ── ReAct control-token stream filter ───────────────────────────────────────── +# Defense-in-depth (Wave 2 ReAct leak fix, 2026-05-25): +# Even if a build-keyword prompt slips past is_website_change() narrowing, +# the agent's ReAct scaffolding (THOUGHT: / ACTION: / OBSERVATION: / FINAL:) +# MUST NOT reach the user-visible chat bubble. This filter buffers stream +# tokens line-by-line and: +# - drops complete lines that start with THOUGHT:/ACTION:/OBSERVATION: +# - for FINAL: lines, emits only the body after the marker +# - passes through everything else unchanged +# +# Used by chat / reply / narrator labels where the assistant text is shown to +# the user. ReAct internal labels ("coder", "asset") should not stream at all +# (brain_graph.py _react now passes stream_label="" for the LLM call), but this +# filter is a belt-and-braces guarantee. + +_REACT_DROP_PREFIXES = ("THOUGHT:", "ACTION:", "OBSERVATION:") +_REACT_FINAL_RE = re.compile(r"\bFINAL\s*:\s*", re.IGNORECASE) + + +class _ReActStreamFilter: + """Strip ReAct control tokens from streaming text, line by line. + + Tokens arrive in arbitrary chunks, so we buffer until a newline and then + classify the line. Partial trailing text (no newline yet) is held until + either we see more tokens or `flush()` is called. The filter is permissive + on the wire: only well-formed control-token lines are dropped — normal + chat text passes through with no change. + """ + + __slots__ = ("_buf", "_after_final", "_seen_any_final") + + def __init__(self): + self._buf: str = "" + self._after_final: bool = False + self._seen_any_final: bool = False + + def feed(self, token: str) -> str: + """Return the portion of `token` that should be emitted downstream.""" + if not token: + return "" + self._buf += token + out_parts: list[str] = [] + # Emit any complete lines, leave a tail in the buffer for next chunk. + while True: + nl = self._buf.find("\n") + if nl < 0: + # No complete line yet. If we are past a FINAL: marker and + # the partial buffer doesn't look like a fresh directive, we + # can still emit it incrementally so the UI streams cleanly. + if self._after_final and self._buf and not self._buf.lstrip().startswith( + _REACT_DROP_PREFIXES + ("FINAL:",)): + out_parts.append(self._buf) + self._buf = "" + break + line = self._buf[: nl + 1] # include trailing \n + self._buf = self._buf[nl + 1:] + stripped = line.lstrip() + if stripped.startswith(_REACT_DROP_PREFIXES): + # Drop the whole line, including its trailing newline. + self._after_final = False + continue + m = _REACT_FINAL_RE.match(stripped) if stripped else None + if m: + body = stripped[m.end():] # text after "FINAL:" + out_parts.append(body) + self._after_final = True + self._seen_any_final = True + continue + # Normal line — emit as-is. + out_parts.append(line) + return "".join(out_parts) + + def flush(self) -> str: + """Return any remaining buffered text (called at stream end).""" + if not self._buf: + return "" + stripped = self._buf.lstrip() + if stripped.startswith(_REACT_DROP_PREFIXES): + self._buf = "" + return "" + m = _REACT_FINAL_RE.match(stripped) if stripped else None + if m: + body = stripped[m.end():] + self._buf = "" + self._seen_any_final = True + return body + out = self._buf + self._buf = "" + return out + +try: + from litellm import completion as _litellm_completion + _LITELLM_OK = True +except ImportError: + _LITELLM_OK = False + +# ── Local-direct fallback config (overridable via /opt/agent-brain/.env) ────── +# Used as Stage 4: bypasses LiteLLM proxy and calls LM Studio directly via HTTP. +# Default points at the Tailnet LM Studio endpoint that the brain aliases already use. +LOCAL_DIRECT_URL = os.getenv( + "LOCAL_LLM_DIRECT_URL", + "http://127.0.0.1:11434/v1/chat/completions", +) +LOCAL_DIRECT_MODEL = os.getenv( + "LOCAL_LLM_DIRECT_MODEL", + "qwen2.5-coder:3b", +) +LOCAL_OLLAMA_CHAT_URL = os.getenv( + "LOCAL_OLLAMA_CHAT_URL", + "http://127.0.0.1:11434/api/chat", +) +LOCAL_CHAT_MAX_TOKENS = int(os.getenv("LOCAL_CHAT_MAX_TOKENS", "512")) + +# User-facing message shown when EVERY LLM path failed. Friendly, voice-safe (no +# stack traces, no tokens, no URLs). +_NO_LLM_USER_MSG = ( + "I'm having trouble reaching my language model right now. " + "Please try again in a moment." +) + +# ── Simple response cache (LRU-ish, 128 slots) ───────────────────────────────── +_cache: dict = {} +_cache_lock = threading.Lock() +_CACHE_MAX = 128 +_CACHE_TTL = 300 # seconds + + +def _cache_key(model: str, prompt: str, system: str) -> str: + # NOTE: hash FULL system prompt — quick_reply/invoke splice session + # history into system prompt after byte 80; truncating caused cross-session + # cache collisions (Wave 1g finding 2026-05-25). Prompt truncation kept + # since user msgs are bounded by chat UI. + sys_hash = hashlib.md5(system.encode(errors='replace')).hexdigest()[:16] + raw = f"{model}|{sys_hash}|{prompt[:200]}" + return hashlib.md5(raw.encode()).hexdigest() + + +def _cache_get(key: str) -> str | None: + with _cache_lock: + entry = _cache.get(key) + if entry and (time.monotonic() - entry["ts"]) < _CACHE_TTL: + return entry["value"] + return None + + +def _cache_put(key: str, value: str): + with _cache_lock: + if len(_cache) >= _CACHE_MAX: + oldest = min(_cache, key=lambda k: _cache[k]["ts"]) + del _cache[oldest] + _cache[key] = {"value": value, "ts": time.monotonic()} + + +# ── Core litellm call (single attempt) ──────────────────────────────────────── +def _litellm_call(model: str, msgs: list, stream: bool = False, + timeout: int = 300, q=None, stream_label: str = "") -> str: + if not _LITELLM_OK: + return "" + if stream: + collected = [] + reasoning_collected = [] + # ReAct sanitizer — strips THOUGHT:/ACTION:/OBSERVATION:/FINAL: prefixes + # from user-visible token stream. The `collected` buffer still gets the + # RAW tokens so the caller's full-text return value (used by _react to + # parse ACTION lines / FINAL bodies) is unaffected. + react_filter = _ReActStreamFilter() if (q and stream_label) else None + r = _litellm_completion( + model=f"openai/{model}", messages=msgs, + api_base=LLM_BASE, api_key="local", timeout=timeout, + stream=True, max_tokens=4096) + for chunk in r: + delta = chunk.choices[0].delta if chunk.choices else None + if delta: + token = getattr(delta, "content", "") or "" + reasoning = getattr(delta, "reasoning_content", "") or "" + if token: + collected.append(token) + if react_filter is not None: + clean = react_filter.feed(token) + if clean: + emit(q, "token", token=clean, msg=clean, label=stream_label) + elif reasoning: + reasoning_collected.append(reasoning) + # Flush any trailing buffered text (last partial line w/o newline). + if react_filter is not None: + tail = react_filter.flush() + if tail: + emit(q, "token", token=tail, msg=tail, label=stream_label) + result = "".join(collected).strip() + if not result: + result = "".join(reasoning_collected).strip() + return result + else: + r = _litellm_completion( + model=f"openai/{model}", messages=msgs, + api_base=LLM_BASE, api_key="local", timeout=timeout, + max_tokens=4096) + msg_obj = r.choices[0].message + text = (msg_obj.content or "").strip() + if not text: + text = (getattr(msg_obj, "reasoning_content", "") or "").strip() + return text + + +# ── Stage 4: direct local model call (no LiteLLM) ───────────────────────────── +def _local_direct_call(prompt: str, system: str = "", timeout: int = 60, + max_tokens: int | None = None, *, q=None, + stream_label: str = "") -> str: + """Final safety net: bypass LiteLLM proxy and call the local model directly. + + Production note (2026-07-31): normal chat must stream visible tokens from + Ollama's native /api/chat. The VPS model is slow per token; waiting for the + full response makes the UI look frozen and lets upstream clients time out. + """ + msgs = [] + if system: + msgs.append({"role": "system", "content": system}) + msgs.append({"role": "user", "content": prompt}) + token_budget = int(max_tokens or LOCAL_CHAT_MAX_TOKENS) + should_stream = bool(q is not None and stream_label) + + native_payload = json.dumps({ + "model": LOCAL_DIRECT_MODEL, + "messages": msgs, + "stream": should_stream, + "keep_alive": os.getenv("LOCAL_OLLAMA_KEEP_ALIVE", "10m"), + "options": {"num_predict": token_budget, "temperature": 0.5}, + }).encode("utf-8") + collected: list[str] = [] + try: + req = urllib.request.Request( + LOCAL_OLLAMA_CHAT_URL, + data=native_payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=timeout) as r: + if should_stream: + for raw in r: + if not raw or not raw.strip(): + continue + try: + data = json.loads(raw.decode("utf-8", errors="replace")) + except Exception: + continue + token = ((data.get("message") or {}).get("content") + or data.get("response") or "") + if token: + collected.append(token) + emit(q, "token", token=token, msg=token, + label=stream_label) + if data.get("done"): + break + text = "".join(collected).strip() + else: + data = json.loads(r.read().decode("utf-8", errors="replace")) + text = ((data.get("message") or {}).get("content") + or data.get("response") or "").strip() + if text: + return text + except Exception as e: + partial = "".join(collected).strip() + if partial: + sys.stderr.write( + f"[brain_llm ollama-native stream partial after {type(e).__name__}: {e}]\n") + return partial + sys.stderr.write( + f"[brain_llm ollama-native direct failed: {type(e).__name__}: {e}]\n") + + payload = json.dumps({ + "model": LOCAL_DIRECT_MODEL, + "messages": msgs, + "max_tokens": token_budget, + "temperature": 0.5, + }).encode("utf-8") + try: + req = urllib.request.Request( + LOCAL_DIRECT_URL, + data=payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=timeout) as r: + data = json.loads(r.read().decode("utf-8", errors="replace")) + text = ((data.get("choices") or [{}])[0].get("message", {}).get("content") or "").strip() + text = text or _NO_LLM_USER_MSG + if q is not None and stream_label and text: + emit(q, "token", token=text, msg=text, label=stream_label) + return text + except Exception as e: + sys.stderr.write(f"[brain_llm local-direct fallback failed: {type(e).__name__}: {e}]\n") + if q is not None and stream_label: + emit(q, "token", token=_NO_LLM_USER_MSG, msg=_NO_LLM_USER_MSG, + label=stream_label) + return _NO_LLM_USER_MSG + + +def llm_chat_direct(prompt: str, system: str = "", *, q=None, + stream_label: str = "", agent_name: str = "", + timeout: int = 75, max_tokens: int = 96) -> str: + """Fast path for user-facing conversational chat. + + Streams native Ollama tokens into the SSE queue when one is provided. This + keeps the UI visibly alive even on the current slower VPS model. + """ + if agent_name: + agent_set(agent_name, "working", prompt[:60], 50, FAST) + result = _local_direct_call(prompt, system, timeout=timeout, + max_tokens=max_tokens, q=q, + stream_label=stream_label) + result = result or _NO_LLM_USER_MSG + if agent_name: + status = "done" if result and result != _NO_LLM_USER_MSG else "error" + agent_set(agent_name, status, result[:60], 100, FAST) + return result + + +# ── Public LLM interface ─────────────────────────────────────────────────────── +def llm(model: str, prompt: str, system: str = "", *, + q=None, stream_label: str = "", agent_name: str = "", + use_cache: bool = False, timeout: int = 300) -> str: + """ + 4-stage fallback (no Anthropic dependency): + 1. LiteLLM streaming (model → heavy-coder / fast-agent / vision) + 2. LiteLLM non-stream (same model, no stream) + 3. LiteLLM fast-agent (if HEAVY failed, retry with FAST) + 4. Direct LM Studio (bypass LiteLLM proxy entirely) + """ + if agent_name: + agent_set(agent_name, "working", prompt[:60], 50, model) + + if use_cache: + ck = _cache_key(model, prompt, system) + cached = _cache_get(ck) + if cached: + if agent_name: + agent_set(agent_name, "done", cached[:60], 100, model) + return cached + + msgs = [] + if system: + msgs.append({"role": "system", "content": system}) + msgs.append({"role": "user", "content": prompt}) + + text = "" + + # Stage 1 — litellm streaming + try: + text = _litellm_call(model, msgs, stream=True, timeout=timeout, + q=q, stream_label=stream_label) + except Exception as e: + sys.stderr.write(f"[brain_llm stage1 stream failed model={model}: {type(e).__name__}: {e}]\n") + + # Stage 2 — litellm non-streaming + if not text: + try: + text = _litellm_call(model, msgs, stream=False, timeout=timeout) + except Exception as e: + sys.stderr.write(f"[brain_llm stage2 nonstream failed model={model}: {type(e).__name__}: {e}]\n") + + # Stage 3 — try fast-agent if heavy-coder failed + if not text and model == HEAVY: + try: + text = _litellm_call(FAST, msgs, stream=False, timeout=60) + except Exception as e: + sys.stderr.write(f"[brain_llm stage3 fast-retry failed: {type(e).__name__}: {e}]\n") + + # Stage 4 — direct LM Studio fallback (bypasses LiteLLM proxy) + if not text: + text = _local_direct_call(prompt, system, timeout=60) + + result = text or _NO_LLM_USER_MSG + + if use_cache and result and result != _NO_LLM_USER_MSG: + _cache_put(ck, result) + + if agent_name: + status = "done" if result and result != _NO_LLM_USER_MSG else "error" + agent_set(agent_name, status, result[:60], 100, model) + + return result + + +def llm_json(model: str, prompt: str, system: str = "", **kwargs) -> dict: + """LLM call that forces JSON output and parses it.""" + json_sys = (system + "\n\nRespond ONLY with valid JSON. No prose, no markdown." + if system else "Respond ONLY with valid JSON. No prose, no markdown.") + raw = llm(model, prompt, json_sys, **kwargs) + # try to extract JSON from response + for start in [raw.find("{"), raw.find("[")]: + if start >= 0: + try: + return json.loads(raw[start:]) + except Exception as e: + sys.stderr.write(f"[brain_llm llm_json parse failed: {type(e).__name__}]\n") + # fallback: wrap raw in a dict + return {"raw": raw} + + +def llm_fast(prompt: str, system: str = "", **kwargs) -> str: + # W2: pin English + brevity by default for chat-fast path. Caller-supplied system overrides. + if not system: + system = "Respond in English. Be concise and friendly." + # W3: enable response cache by default for fast path (cache infra already exists, was disabled). + kwargs.setdefault("use_cache", True) + return llm(FAST, prompt, system, **kwargs) + + +def llm_heavy(prompt: str, system: str = "", **kwargs) -> str: + if not system: + system = "Respond in English." + return llm(HEAVY, prompt, system, **kwargs) diff --git a/vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh b/vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh new file mode 100644 index 00000000000..66fd74bb6aa --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +LITELLM_BASE="${LITELLM_BASE:-http://127.0.0.1:4000}" +MODEL="${LITELLM_HEALTH_MODEL:-fast-agent}" +ENV_FILE="${LITELLM_ENV_FILE:-/opt/litellm/.env}" + +if [ -f "$ENV_FILE" ]; then + set -a + # shellcheck disable=SC1090 + . "$ENV_FILE" + set +a +fi + +echo "Checking LiteLLM readiness at ${LITELLM_BASE}/health/readiness" +curl -fsS --max-time 10 "${LITELLM_BASE}/health/readiness" >/dev/null + +echo "Checking LiteLLM liveliness at ${LITELLM_BASE}/health/liveliness" +curl -fsS --max-time 10 "${LITELLM_BASE}/health/liveliness" >/dev/null + +echo "Checking LiteLLM model list at ${LITELLM_BASE}/v1/models" +curl -fsS --max-time 10 "${LITELLM_BASE}/v1/models" >/dev/null + +echo "Checking LiteLLM completion path with model ${MODEL}" +AUTH_ARGS=() +if [ -n "${LITELLM_MASTER_KEY:-}" ]; then + AUTH_ARGS=(-H "Authorization: Bearer ${LITELLM_MASTER_KEY}") +else + echo "LITELLM_MASTER_KEY is not set; probing unauthenticated local completion path." +fi + +curl -fsS --max-time 60 "${LITELLM_BASE}/v1/chat/completions" \ + "${AUTH_ARGS[@]}" \ + -H "Content-Type: application/json" \ + -d "{\"model\":\"${MODEL}\",\"messages\":[{\"role\":\"user\",\"content\":\"reply pong only\"}],\"max_tokens\":8,\"stream\":false}" \ + | python3 -c "import json,sys; data=json.load(sys.stdin); assert data.get('choices'), data; print(data['choices'][0]['message'].get('content','')[:80])" + +echo "LiteLLM functional health passed." diff --git a/vps/patches/daveai-production-runtime-20260731/nginx/00-kilo-bot-protection.conf b/vps/patches/daveai-production-runtime-20260731/nginx/00-kilo-bot-protection.conf new file mode 100644 index 00000000000..e5e50282448 --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/nginx/00-kilo-bot-protection.conf @@ -0,0 +1,31 @@ +# Kilo/DaveAI origin bot guard. +# Cloudflare should remain the primary WAF. These nginx guards are a +# reversible origin fallback for obvious scanner probes and login floods. + +map $http_cf_connecting_ip $kilo_client_key { + default $http_cf_connecting_ip; + "" $binary_remote_addr; +} + +map $uri $kilo_bad_probe { + default 0; + ~*^/(?:\.env|\.git|\.svn|\.hg|\.DS_Store|\.codex|auth\.json)$ 1; + ~*^/(?:wp-admin|wp-content|wp-includes|wp-json|wp-login\.php|xmlrpc\.php)(?:/|$) 1; + ~*^/(?:phpmyadmin|pma|adminer)(?:/|$) 1; + ~*^/(?:v1/messages|v1/chat/completions|v1/models|v1beta/models)$ 1; +} + +map $http_user_agent $kilo_bad_ua { + default 0; + ~*(?:sqlmap|nikto|masscan|zgrab|nuclei|LLMGatewayScan) 1; +} + +map $arg_return_to $kilo_bad_return_to { + default 0; + ~*(?:/\.env|/\.git|wp-admin|wp-content|wp-includes|wp-json|wp-login\.php|xmlrpc\.php) 1; + ~*(?:mysql|mongodb|redis|database|db|backup|dump|passwd|shadow) 1; + ~*\.(?:sql|sqlite|db|bak|old|tar|tgz|gz|bz2|xz|zst|zip|rar|7z)(?:$|[?#]) 1; +} + +limit_req_zone $kilo_client_key zone=kilo_login_guard:10m rate=60r/m; +limit_req_status 429; diff --git a/vps/patches/daveai-production-runtime-20260731/nginx/brain.daveai.tech b/vps/patches/daveai-production-runtime-20260731/nginx/brain.daveai.tech new file mode 100644 index 00000000000..ca1406157d9 --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/nginx/brain.daveai.tech @@ -0,0 +1,40 @@ +server { + listen 80; + listen [::]:80; + server_name brain.daveai.tech; + + location ^~ /.well-known/acme-challenge/ { + root /var/www/letsencrypt; + default_type text/plain; + } + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name brain.daveai.tech; + + ssl_certificate /etc/nginx/ssl/daveai.tech/cloudflare-origin.pem; + ssl_certificate_key /etc/nginx/ssl/daveai.tech/cloudflare-origin.key; + ssl_protocols TLSv1.2 TLSv1.3; + + add_header X-Content-Type-Options "nosniff" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + access_log /var/log/nginx/brain-access.log combined; + error_log /var/log/nginx/brain-error.log warn; + + location / { + proxy_pass http://127.0.0.1:8888; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_buffering off; + } +} diff --git a/vps/patches/daveai-production-runtime-20260731/nginx/daveai.tech b/vps/patches/daveai-production-runtime-20260731/nginx/daveai.tech new file mode 100644 index 00000000000..25564f7e889 --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/nginx/daveai.tech @@ -0,0 +1,348 @@ +# daveai.tech nginx reverse proxy +# Cloudflare handles SSL (Full mode) this listens on port 80 +# If you add Let's Encrypt: run certbot --nginx -d daveai.tech + +# Rate limiting: 10 requests/sec per IP for API, 2/sec for admin login +limit_req_zone $kilo_client_key zone=api:10m rate=80r/s; +limit_req_zone $kilo_client_key zone=login:10m rate=2r/s; + +server { + if ($kilo_bad_probe) { return 444; } + if ($kilo_bad_ua) { return 444; } + if ($kilo_bad_return_to) { return 444; } + + location ^~ /.well-known/acme-challenge/ { + root /var/www/letsencrypt; + default_type text/plain; + } + + listen 80; + listen 443 ssl; + server_name daveai.tech; + + # SSL certs for Cloudflare Full mode + ssl_certificate /etc/letsencrypt/live/daveai-root/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/daveai-root/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + client_max_body_size 50M; + proxy_read_timeout 300s; + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com https://cdnjs.cloudflare.com; connect-src 'self' https://daveai.tech wss://daveai.tech https://api-inference.huggingface.co https://worldtimeapi.org; media-src 'self' blob:;" always; + add_header Permissions-Policy "geolocation=(), microphone=(self), camera=()" always; + + # Block private Playwright artifact / reference store from public access + # (per wave2-handoffs/16-PLAYWRIGHT-HERMES-VISUAL-E2E.md §6). + # All paths beginning with __ (double-underscore) are admin-only and must + # only be served via /api/playwright/artifact/* with admin JWT. + location ^~ /__ { + return 444; + } + + # Import preview assets are API responses even when they end in .html/.css. + # Keep this ^~ block above regex static locations so tokenized preview files + # reach FastAPI instead of being treated as public website assets. + location ^~ /api/imports/preview-asset/ { + limit_req zone=api burst=160 nodelay; + proxy_pass http://127.0.0.1:8888/imports/preview-asset/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 60s; + proxy_hide_header Content-Security-Policy; + proxy_hide_header X-Frame-Options; + proxy_hide_header X-Content-Type-Options; + add_header Cache-Control "no-store" always; + add_header Content-Security-Policy "default-src 'none'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; media-src 'self' data:; script-src 'none'; connect-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + } + + # Published DaveAI help and proof documents. + location ^~ /docs/ { + root /var/www/agentic-website; + try_files $uri =404; + default_type text/plain; + } + + # Public previews for isolated DaveAI projects. Only a project landing + # route and browser-safe static asset extensions are served; dotfiles, + # Git data, source secrets, and arbitrary workspace files fall through to + # the 404 location below. + location ~ ^/user-projects/user-[0-9]+/[0-9]+-[a-z0-9-]+/?$ { + root /var/www/agentic-website; + try_files $uri/index.html =404; + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + location ~* ^/user-projects/user-[0-9]+/[0-9]+-[a-z0-9-]+/(?:[A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+\.(?:html?|css|js|mjs|cjs|json|map|png|jpe?g|gif|ico|svg|webp|avif|woff2?|ttf|wasm|txt|mp3|wav|ogg|mp4|webm)$ { + root /var/www/agentic-website; + try_files $uri =404; + expires -1; + add_header Cache-Control "no-cache, must-revalidate"; + } + + location /user-projects/ { + return 404; + } + + # HTML files: no-cache so updates deploy instantly + location ~* \.html$ { + root /var/www/agentic-website; + try_files $uri $uri/ @nextjs; + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + } + + # Other static assets (CSS, JS, images, fonts, media) — cached 1h + location ~* \.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|webp|mp3|wav|mp4|webm|ogg)$ { + root /var/www/agentic-website; + try_files $uri $uri/ @nextjs; + expires 1h; + add_header Cache-Control "public, must-revalidate"; + access_log off; + } + + # Named fallback to Next.js + location @nextjs { + proxy_pass http://127.0.0.1:3001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + # Plain /login was receiving high bot/scanner volume. Keep it routed to + # the app, but throttle by CF-Connecting-IP when Cloudflare is present. + location = /login { + limit_req zone=kilo_login_guard burst=60 nodelay; + proxy_pass http://127.0.0.1:3001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Static game files (served from /var/www/agentic-website/games/) + location /games/ { + root /var/www/agentic-website; + index index.html; + try_files $uri $uri/ $uri/index.html =404; + expires 1h; + add_header Cache-Control "public, must-revalidate"; + } + + # Chat attachment uploads (Wave B 2026-05-25 — brain_uploads_api). + # Stored by POST /api/uploads/image -> randomized uuid4. filenames. + # See /etc/nginx/conf.d/upload-static.conf for the canonical reference. + # STOP-4: nosniff + only image extensions served (everything else 404). + location /uploads/ { + alias /var/www/agentic-website/uploads/; + autoindex off; + add_header X-Content-Type-Options "nosniff" always; + add_header Cache-Control "public, max-age=86400" always; + add_header Referrer-Policy "no-referrer" always; + location ~* ^/uploads/[A-Za-z0-9]+\.(png|jpg|jpeg|webp|gif)$ { + try_files $uri =404; + } + location ~ /uploads/ { + return 404; + } + } + + # Root path: serve index.html (redirects to daveai-ui-v6.html with video intro) + location = / { + root /var/www/agentic-website; + index index.html; + try_files /index.html =404; + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # Open WebUI - Hermes Chat Interface + location /hermes/ { + proxy_pass http://localhost:7860/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 300s; + client_max_body_size 100m; + proxy_buffering off; + sub_filter '' ''; + sub_filter_once on; + } + location ~* ^/(?!hermes)(_app|static|assets|favicon)/ { + proxy_pass http://localhost:7860; + } + + # Chat UI Next.js on :3001 + location / { + proxy_pass http://127.0.0.1:3001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Admin login — strict rate limit (2 req/sec burst 5) + location = /api/admin/login { + limit_req zone=login burst=5 nodelay; + proxy_pass http://127.0.0.1:8888/admin/login; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # Auth login/register — rate limited (2 req/sec burst 5) + location = /api/auth/login { + limit_req zone=login burst=5 nodelay; + proxy_pass http://127.0.0.1:8888/auth/login; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location = /api/auth/register { + limit_req zone=login burst=5 nodelay; + proxy_pass http://127.0.0.1:8888/auth/register; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # SSE streaming endpoint — must come before /api/ block + location = /api/stream { + proxy_pass http://127.0.0.1:8888/stream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + add_header X-Accel-Buffering no; + chunked_transfer_encoding on; + } + + # Edge TTS — voice synthesis proxy (port 5050) + # Prefix match so /api/tts, /api/tts/health, /api/edge-tts/health etc all route here + location = /api/tts { + default_type application/json; + add_header Cache-Control "no-store" always; + return 410 '{"error":"voice_route_retired","replacement":"/api/azure-tts"}'; + } + + location = /api/edge-tts { + default_type application/json; + add_header Cache-Control "no-store" always; + return 410 '{"error":"voice_route_retired","replacement":"/api/azure-tts"}'; + } + + location = /api/voices { + proxy_pass http://127.0.0.1:8888/voices; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /api/agents/voices { + default_type application/json; + add_header Cache-Control "no-store" always; + return 410 '{"error":"voice_route_retired","replacement":"/api/voices"}'; + } + + + # Public health endpoint for frontend liveness checks — intentionally not API-rate-limited. + location = /api/health { + proxy_pass http://127.0.0.1:8888/health; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + add_header Cache-Control "no-store" always; + } + + # Agent Brain REST API strips /api prefix + # /api/chat :8888/chat | /api/health :8888/health + location /api/ { + limit_req zone=api burst=160 nodelay; + proxy_pass http://127.0.0.1:8888/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 300s; + } + + # WebSocket real-time chat + location /ws { + proxy_pass http://127.0.0.1:8888/ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + } + + # ZeroClaw gateway localhost only (internal tooling) + location /gateway/ { + allow 127.0.0.1; + deny all; + proxy_pass http://127.0.0.1:3000/; + } + + # Health check endpoint + location = /health { + proxy_pass http://127.0.0.1:8888/health; + } + + # Next.js static assets cache aggressively + location /_next/static/ { + proxy_pass http://127.0.0.1:3001; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + # Custom error pages + error_page 404 /404.html; + location = /404.html { + root /var/www/agentic-website; + internal; + } + + error_page 502 503 504 /50x.html; + location = /50x.html { + root /var/www/agentic-website; + internal; + } +} diff --git a/vps/patches/daveai-production-runtime-20260731/nginx/iptv.daveai.tech b/vps/patches/daveai-production-runtime-20260731/nginx/iptv.daveai.tech new file mode 100644 index 00000000000..dce51014e2a --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/nginx/iptv.daveai.tech @@ -0,0 +1,143 @@ +# Wave 8 — iptv.daveai.tech (new vhost, fronts iptv-restream Docker on :3103) +# Backend: 127.0.0.1:3103 (Docker container iptv-restream-iptv_restream_nginx-1) + +server { + listen 80; + listen [::]:80; + server_name iptv.daveai.tech; + + location ^~ /.well-known/acme-challenge/ { + root /var/www/letsencrypt; + default_type text/plain; + } + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name iptv.daveai.tech; + + ssl_certificate /etc/nginx/ssl/daveai.tech/cloudflare-origin.pem; + ssl_certificate_key /etc/nginx/ssl/daveai.tech/cloudflare-origin.key; + ssl_protocols TLSv1.2 TLSv1.3; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + access_log /var/log/nginx/iptv-access.log combined; + error_log /var/log/nginx/iptv-error.log warn; + + client_max_body_size 50M; + + location = /auth/check { + internal; + proxy_pass http://davetv_auth_gate/auth/check; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Original-Host $host; + } + + error_page 401 = @davetv_login_redirect; + + location @davetv_login_redirect { + return 302 https://apps.daveai.tech/login?return_to=$scheme://$host$request_uri; + } + + location = /login { + proxy_pass http://davetv_auth_gate; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + location = /logout { + proxy_pass http://davetv_auth_gate; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + location = /session { + proxy_pass http://davetv_auth_gate; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + location = /auth/session { + proxy_pass http://davetv_auth_gate; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + + location = /api/provider-vault/providers { + auth_request /auth/check; + auth_request_set $davetv_user $upstream_http_x_daveai_user; + auth_request_set $davetv_email $upstream_http_x_daveai_email; + error_page 401 403 = @provider_vault_empty_providers; + proxy_pass http://127.0.0.1:3102; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-DaveTV-Authenticated 1; + proxy_set_header X-DaveAI-User $davetv_user; + proxy_set_header X-DaveAI-Email $davetv_email; + } + + location @provider_vault_empty_providers { + default_type application/json; + add_header Cache-Control "no-store" always; + return 200 '{"providers":[]}'; + } + + location ^~ /api/provider-vault/ { + auth_request /auth/check; + auth_request_set $davetv_user $upstream_http_x_daveai_user; + auth_request_set $davetv_email $upstream_http_x_daveai_email; + proxy_pass http://127.0.0.1:3102; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-DaveTV-Authenticated 1; + proxy_set_header X-DaveAI-User $davetv_user; + proxy_set_header X-DaveAI-Email $davetv_email; + proxy_set_header Range $http_range; + proxy_set_header If-Range $http_if_range; + proxy_buffering off; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + } + + location / { + proxy_pass http://127.0.0.1:3103; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_buffering off; + } +} diff --git a/vps/patches/daveai-production-runtime-20260731/nginx/stories.daveai.tech b/vps/patches/daveai-production-runtime-20260731/nginx/stories.daveai.tech new file mode 100644 index 00000000000..58e0a53a2e8 --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/nginx/stories.daveai.tech @@ -0,0 +1,136 @@ +map $http_upgrade $stories_connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + listen [::]:80; + server_name stories.daveai.tech; + return 301 https://$host$request_uri; +} + +server { + listen 80; + listen [::]:80; + server_name api.daveai.tech; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name stories.daveai.tech; + client_max_body_size 50m; + proxy_read_timeout 3600s; + + ssl_certificate /etc/letsencrypt/live/stories.daveai.tech/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/stories.daveai.tech/privkey.pem; + + location ~ ^/(auth|rest)/v1/ { + proxy_pass http://127.0.0.1:18000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } + + location = /graphql/v1 { + proxy_pass http://127.0.0.1:18000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } + + location /storage/v1/ { + proxy_pass http://127.0.0.1:18000; + proxy_http_version 1.1; + proxy_request_buffering off; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } + + location /functions/v1/ { + proxy_pass http://127.0.0.1:18000; + proxy_http_version 1.1; + proxy_buffering off; + proxy_read_timeout 3600s; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } + + location /realtime/v1/ { + proxy_pass http://127.0.0.1:18000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $stories_connection_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 3600s; + } + + location /local-rig/ { + if ($http_x_bridge_token != "552e5787c1030d4b56077adfc504897c116287c59dc7314b") { return 403; } + proxy_pass https://127.0.0.1:18788/; + proxy_ssl_verify off; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 900s; + } + + location / { + proxy_pass http://127.0.0.1:18080; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name api.daveai.tech; + client_max_body_size 50m; + proxy_read_timeout 3600s; + + ssl_certificate /etc/letsencrypt/live/stories.daveai.tech/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/stories.daveai.tech/privkey.pem; + + + location = /health { + default_type application/json; + add_header Cache-Control "no-store" always; + return 200 '{"status":"ok","service":"api.daveai.tech","public_health":true}'; + } + + location / { + proxy_pass http://127.0.0.1:18000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $stories_connection_upgrade; + } +} diff --git a/vps/patches/daveai-production-runtime-20260731/nginx/voice.daveai.tech b/vps/patches/daveai-production-runtime-20260731/nginx/voice.daveai.tech new file mode 100644 index 00000000000..107cd784885 --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/nginx/voice.daveai.tech @@ -0,0 +1,107 @@ +# Wave 8 — voice.daveai.tech (HTTPS via Cloudflare Origin wildcard) +# Backends: edge-tts on 127.0.0.1:5050, model server on 127.0.0.1:8080 + +limit_req_zone $binary_remote_addr zone=voice_synth_limit:10m rate=10r/s; +limit_req_zone $binary_remote_addr zone=voice_api_limit:10m rate=30r/s; + +upstream voice_edge_tts_backend { + server 127.0.0.1:5050; +} + +upstream voice_model_backend { + server 127.0.0.1:8080; +} + +server { + listen 80; + listen [::]:80; + server_name voice.daveai.tech; + + location ^~ /.well-known/acme-challenge/ { + root /var/www/letsencrypt; + default_type text/plain; + } + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name voice.daveai.tech; + + ssl_certificate /etc/nginx/ssl/daveai.tech/cloudflare-origin.pem; + ssl_certificate_key /etc/nginx/ssl/daveai.tech/cloudflare-origin.key; + ssl_protocols TLSv1.2 TLSv1.3; + + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; + add_header Access-Control-Allow-Headers "*" always; + + access_log /var/log/nginx/voice-access.log combined; + error_log /var/log/nginx/voice-error.log warn; + + # CORS preflight + location @cors_preflight { + add_header Access-Control-Allow-Origin "*"; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; + add_header Access-Control-Allow-Headers "*"; + add_header Content-Length 0; + add_header Content-Type text/plain; + return 204; + } + + + location = /health { + proxy_pass http://voice_edge_tts_backend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 30s; + } + + # Synthesize gets tighter rate limit (rewrite /api/X -> /X for upstream) + location = /api/synthesize { + limit_req zone=voice_synth_limit burst=5 nodelay; + proxy_pass http://voice_edge_tts_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 30s; + } + + # API endpoints -> edge-tts-server (rewrite /api/X -> /X for upstream) + location /api/ { + limit_req zone=voice_api_limit burst=20 nodelay; + proxy_pass http://voice_edge_tts_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 60s; + } + + # Model downloads -> model-server + location /models/ { + limit_req zone=voice_api_limit burst=10 nodelay; + proxy_pass http://voice_model_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 300s; + } + + # Root: voice landing page + location = / { + root /var/www/agentic-website; + index voice-landing.html; + try_files /voice-landing.html =404; + } +} diff --git a/vps/patches/daveai-production-runtime-20260731/td2/generated-assets-manifest.txt b/vps/patches/daveai-production-runtime-20260731/td2/generated-assets-manifest.txt new file mode 100644 index 00000000000..7de39e34d11 --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/td2/generated-assets-manifest.txt @@ -0,0 +1,150 @@ +assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Battle_theme_Halloween_looped.mp3 +assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Battle_theme_exclusion_zone_looped.mp3 +assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Main_theme_Halloween_looped.mp3 +assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Main_theme_exclusion_zone_alternative_looped.mp3 +assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Main_theme_exclusion_zone_looped.mp3 +assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Alternative_theme_Chinese_Street.mp3 +assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Battle_theme_Chinese_Street.mp3 +assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Calm_theme_Chinese_Street.mp3 +assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Main_theme_Chinese_Street.mp3 +assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Stealthy_theme_loopable.mp3 +assets/vfx/damage/1_effect_damage_000.png +assets/vfx/damage/1_effect_damage_001.png +assets/vfx/damage/1_effect_damage_002.png +assets/vfx/damage/1_effect_damage_003.png +assets/vfx/damage/1_effect_damage_004.png +assets/vfx/damage/1_effect_damage_005.png +assets/vfx/damage/1_effect_damage_006.png +assets/vfx/damage/1_effect_damage_007.png +assets/vfx/damage/1_effect_damage_008.png +assets/vfx/damage/1_effect_damage_009.png +assets/vfx/def/1_effect_def_000.png +assets/vfx/def/1_effect_def_001.png +assets/vfx/def/1_effect_def_002.png +assets/vfx/def/1_effect_def_003.png +assets/vfx/def/1_effect_def_004.png +assets/vfx/def/1_effect_def_005.png +assets/vfx/def/1_effect_def_006.png +assets/vfx/def/1_effect_def_007.png +assets/vfx/def/1_effect_def_008.png +assets/vfx/def/1_effect_def_009.png +assets/vfx/fire/1_effect_fire_000.png +assets/vfx/fire/1_effect_fire_001.png +assets/vfx/fire/1_effect_fire_002.png +assets/vfx/fire/1_effect_fire_003.png +assets/vfx/fire/1_effect_fire_004.png +assets/vfx/fire/1_effect_fire_005.png +assets/vfx/fire/1_effect_fire_006.png +assets/vfx/fire/1_effect_fire_007.png +assets/vfx/fire/1_effect_fire_008.png +assets/vfx/fire/1_effect_fire_009.png +assets/vfx/fire/1_effect_fire_010.png +assets/vfx/fire/1_effect_fire_011.png +assets/vfx/fire/1_effect_fire_012.png +assets/vfx/fire/1_effect_fire_013.png +assets/vfx/fire/1_effect_fire_014.png +assets/vfx/fire/1_effect_fire_015.png +assets/vfx/fire/1_effect_fire_016.png +assets/vfx/fire/1_effect_fire_017.png +assets/vfx/fire/1_effect_fire_018.png +assets/vfx/freeze/1_effect_freeze_000.png +assets/vfx/freeze/1_effect_freeze_001.png +assets/vfx/freeze/1_effect_freeze_002.png +assets/vfx/freeze/1_effect_freeze_003.png +assets/vfx/freeze/1_effect_freeze_004.png +assets/vfx/freeze/1_effect_freeze_005.png +assets/vfx/freeze/1_effect_freeze_006.png +assets/vfx/freeze/1_effect_freeze_007.png +assets/vfx/freeze/1_effect_freeze_008.png +assets/vfx/freeze/1_effect_freeze_009.png +assets/vfx/freeze/1_effect_freeze_010.png +assets/vfx/freeze/1_effect_freeze_011.png +assets/vfx/freeze/1_effect_freeze_012.png +assets/vfx/freeze/1_effect_freeze_013.png +assets/vfx/freeze/1_effect_freeze_014.png +assets/vfx/freeze/1_effect_freeze_015.png +assets/vfx/rain/1_effect_rain_001.png +assets/vfx/rain/1_effect_rain_002.png +assets/vfx/rain/1_effect_rain_003.png +assets/vfx/rain/1_effect_rain_004.png +assets/vfx/rain/1_effect_rain_005.png +assets/vfx/rain/1_effect_rain_006.png +assets/vfx/rain/1_effect_rain_007.png +assets/vfx/rain/1_effect_rain_008.png +assets/vfx/rain/1_effect_rain_009.png +assets/vfx/rain/1_effect_rain_010.png +assets/vfx/rain/1_effect_rain_011.png +assets/vfx/rain/1_effect_rain_012.png +assets/vfx/rain/1_effect_rain_013.png +assets/vfx/rain/1_effect_rain_014.png +assets/vfx/rain/1_effect_rain_015.png +assets/vfx/rain/1_effect_rain_016.png +assets/vfx/rain/1_effect_rain_017.png +assets/vfx/rain/1_effect_rain_018.png +assets/vfx/rain/1_effect_rain_019.png +assets/vfx/rain/1_effect_rain_020.png +assets/vfx/rain/1_effect_rain_021.png +assets/vfx/rain/1_effect_rain_022.png +assets/vfx/rain/1_effect_rain_023.png +assets/vfx/rain/1_effect_rain_024.png +assets/vfx/rain/1_effect_rain_025.png +assets/vfx/rain/1_effect_rain_026.png +assets/vfx/rain/1_effect_rain_027.png +assets/vfx/rain/1_effect_rain_028.png +assets/vfx/rain/1_effect_rain_029.png +assets/vfx/rain/1_effect_rain_030.png +assets/vfx/rain/1_effect_rain_031.png +assets/vfx/rain/1_effect_rain_032.png +assets/vfx/rain/1_effect_rain_033.png +assets/vfx/rain/1_effect_rain_034.png +assets/vfx/rain/1_effect_rain_035.png +assets/vfx/rain/1_effect_rain_036.png +assets/vfx/rain/1_effect_rain_037.png +assets/vfx/rain/1_effect_rain_038.png +assets/vfx/rain/1_effect_rain_039.png +assets/vfx/stone/1_effect_stone_000.png +assets/vfx/stone/1_effect_stone_001.png +assets/vfx/stone/1_effect_stone_002.png +assets/vfx/stone/1_effect_stone_003.png +assets/vfx/stone/1_effect_stone_004.png +assets/vfx/stone/1_effect_stone_005.png +assets/vfx/stone/1_effect_stone_006.png +assets/vfx/stone/1_effect_stone_007.png +assets/vfx/stone/1_effect_stone_008.png +assets/vfx/stone/1_effect_stone_009.png +assets/vfx/stone/1_effect_stone_010.png +assets/vfx/stone/1_effect_stone_011.png +assets/vfx/stone/1_effect_stone_012.png +assets/vfx/stone/1_effect_stone_013.png +assets/vfx/stone/1_effect_stone_014.png +assets/vfx/stone/1_effect_stone_015.png +assets/vfx/stone/1_effect_stone_016.png +assets/vfx/stone/1_effect_stone_017.png +assets/vfx/time/1_effect_time_000.png +assets/vfx/time/1_effect_time_001.png +assets/vfx/time/1_effect_time_002.png +assets/vfx/time/1_effect_time_003.png +assets/vfx/time/1_effect_time_004.png +assets/vfx/time/1_effect_time_005.png +assets/vfx/time/1_effect_time_006.png +assets/vfx/time/1_effect_time_007.png +assets/vfx/time/1_effect_time_008.png +assets/vfx/time/1_effect_time_009.png +assets/vfx/time/1_effect_time_010.png +assets/vfx/time/1_effect_time_011.png +assets/vfx/time/1_effect_time_012.png +assets/vfx/time/1_effect_time_013.png +assets/vfx/zip/1_effect_zip_000.png +assets/vfx/zip/1_effect_zip_001.png +assets/vfx/zip/1_effect_zip_002.png +assets/vfx/zip/1_effect_zip_003.png +assets/vfx/zip/1_effect_zip_004.png +assets/vfx/zip/1_effect_zip_005.png +assets/vfx/zip/1_effect_zip_006.png +assets/vfx/zip/1_effect_zip_007.png +assets/vfx/zip/1_effect_zip_008.png +assets/vfx/zip/1_effect_zip_009.png +assets/vfx/zip/1_effect_zip_010.png +assets/vfx/zip/1_effect_zip_011.png +assets/vfx/zip/1_effect_zip_012.png +assets/vfx/zip/1_effect_zip_013.png diff --git a/vps/patches/daveai-production-runtime-20260731/td2/index-BEGIhVuy.js b/vps/patches/daveai-production-runtime-20260731/td2/index-BEGIhVuy.js new file mode 100644 index 00000000000..2bb238abaaf --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/td2/index-BEGIhVuy.js @@ -0,0 +1,4 @@ +var dt=Object.defineProperty;var ht=(d,t,e)=>t in d?dt(d,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):d[t]=e;var r=(d,t,e)=>ht(d,typeof t!="symbol"?t+"":t,e);import{P as u}from"./phaser-Cm2tabnc.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const o of i)if(o.type==="childList")for(const a of o.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&s(a)}).observe(document,{childList:!0,subtree:!0});function e(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(i){if(i.ep)return;i.ep=!0;const o=e(i);fetch(i.href,o)}})();class ct extends u.Scene{constructor(){super({key:"BootScene"})}preload(){this.load.setBaseURL("/"),this.createFallbackLoadingBar()}create(){this.scene.start("PreloadScene")}createFallbackLoadingBar(){const{width:t,height:e}=this.cameras.main;this.add.rectangle(t/2,e/2,400,40,2236962),this.add.rectangle(t/2-198,e/2,4,36,65416),this.add.text(t/2,e/2+40,"LOADING...",{fontSize:"18px",color:"#ffffff",fontFamily:"monospace"}).setOrigin(.5)}}const We="daveai-td2-save",pt={musicVolume:.7,sfxVolume:1,quality:"high",uiSkin:"default"},ae={version:"1.0.0",worlds:{"world-01":{unlocked:!0,levels:{}},"world-02":{unlocked:!1,levels:{}},"world-03":{unlocked:!1,levels:{}},"world-04":{unlocked:!1,levels:{}},"world-05":{unlocked:!1,levels:{}},"world-06":{unlocked:!1,levels:{}},"world-07":{unlocked:!0,levels:{}},"world-08":{unlocked:!1,levels:{}},"world-09":{unlocked:!1,levels:{}},"world-10":{unlocked:!1,levels:{}},"world-11":{unlocked:!1,levels:{}},"world-12":{unlocked:!1,levels:{}},"world-13":{unlocked:!1,levels:{}},"world-14":{unlocked:!1,levels:{}},"world-15":{unlocked:!1,levels:{}},"world-16":{unlocked:!1,levels:{}},"world-17":{unlocked:!1,levels:{}},"world-18":{unlocked:!1,levels:{}},"world-19":{unlocked:!1,levels:{}},"world-20":{unlocked:!1,levels:{}},"world-21":{unlocked:!1,levels:{}},"world-22":{unlocked:!1,levels:{}},"world-23":{unlocked:!1,levels:{}},"world-24":{unlocked:!1,levels:{}},"world-25":{unlocked:!1,levels:{}},"world-26":{unlocked:!1,levels:{}},"world-27":{unlocked:!1,levels:{}},"world-28":{unlocked:!1,levels:{}},"world-29":{unlocked:!1,levels:{}},"world-30":{unlocked:!1,levels:{}}},totalGold:0,achievements:[],settings:pt},G=class G{constructor(){r(this,"currentWorldId",1);r(this,"currentLevelId","");r(this,"gold",150);r(this,"lives",20);r(this,"score",0);r(this,"isPaused",!1);r(this,"abilityCharges",{});r(this,"saveData",ae)}static getInstance(){return G.instance||(G.instance=new G),G.instance}load(){try{const t=localStorage.getItem(We);t&&(this.saveData={...ae,...JSON.parse(t)})}catch{this.saveData={...ae}}}save(){try{localStorage.setItem(We,JSON.stringify(this.saveData))}catch{console.warn("Failed to save game state")}}get settings(){return this.saveData.settings}isWorldUnlocked(t){var e;return((e=this.saveData.worlds[t])==null?void 0:e.unlocked)??!1}getLevelData(t,e){var s;return((s=this.saveData.worlds[t])==null?void 0:s.levels[e])??{completed:!1,stars:0,bestTime:0}}completeLevel(t,e,s,i){this.saveData.worlds[t]||(this.saveData.worlds[t]={unlocked:!0,levels:{}});const o=this.saveData.worlds[t].levels[e];this.saveData.worlds[t].levels[e]={completed:!0,stars:Math.max((o==null?void 0:o.stars)??0,s),bestTime:o!=null&&o.bestTime?Math.min(o.bestTime,i):i},this.save()}unlockWorld(t){this.saveData.worlds[t]||(this.saveData.worlds[t]={unlocked:!1,levels:{}}),this.saveData.worlds[t].unlocked=!0,this.save()}reset(){this.saveData={...ae},this.save()}};r(G,"instance");let Ee=G;const _=Ee.getInstance(),je=["idle","walk","run","attack","hurt","die","jump"],ge=10,Ne=20;class H{static preload(t){for(let e=1;e<=ge;e++)for(const s of je)for(let i=0;i0){const a=s==="hurt"?16:s==="die"?12:10,n=s==="walk"||s==="run"||s==="idle"?-1:0;t.anims.create({key:i,frames:o,frameRate:a,repeat:n})}}}static getAnimKey(t,e){return`enemy-${t}-${e}`}static getSpriteTypeFromId(t){const e=t.match(/(\d+)/);return((e?parseInt(e[1],10):1)-1)%ge+1}}const He={archer:76,magic:31,stone:61,support:17};class he{static preload(t){for(const[e,s]of Object.entries(He))for(let i=1;i<=s;i++){const o=`tower-${e}-${String(i).padStart(3,"0")}`;t.load.image(o,`assets/towers/${e}/${i}.png?v=20260731td2assets2`)}}static registerAnimations(t){for(const[e,s]of Object.entries(He)){const i=`tower-${e}-idle`;if(t.anims.exists(i))continue;const o=[];for(let a=1;a<=s;a++){const n=`tower-${e}-${String(a).padStart(3,"0")}`;t.textures.exists(n)&&o.push({key:n})}o.length>0&&t.anims.create({key:i,frames:o,frameRate:8,repeat:-1})}}static getIdleAnimKey(t){return`tower-${t}-idle`}static getFirstFrameKey(t){return`tower-${t}-001`}}var p=(d=>(d.WAVE_STARTED="wave:started",d.WAVE_COMPLETED="wave:completed",d.ALL_WAVES_COMPLETED="wave:all_completed",d.ENEMY_SPAWN_REQUEST="enemy:spawn-request",d.BOSS_SPAWN_REQUEST="boss:spawn-request",d.ENEMY_SPAWNED="enemy:spawned",d.ENEMY_DIED="enemy:died",d.ENEMY_REACHED_END="enemy:reached_end",d.BOSS_PHASE_CHANGED="boss:phase_changed",d.BOSS_DIED="boss:died",d.TOWER_PLACED="tower:placed",d.TOWER_UPGRADED="tower:upgraded",d.TOWER_SOLD="tower:sold",d.TOWER_SELECTED="tower:selected",d.TOWER_DESELECTED="tower:deselected",d.PROJECTILE_SPLASH="projectile:splash",d.GOLD_CHANGED="economy:gold_changed",d.LIVES_CHANGED="economy:lives_changed",d.DAMAGE_DEALT="damage:dealt",d.LEVEL_COMPLETED="level:completed",d.LEVEL_FAILED="level:failed",d.PLAY_MUSIC="audio:play_music",d.PLAY_SFX="audio:play_sfx",d.SET_MUSIC_VOLUME="audio:set_music_volume",d.SET_SFX_VOLUME="audio:set_sfx_volume",d))(p||{});const U=class U extends u.Events.EventEmitter{static getInstance(){return U.instance||(U.instance=new U),U.instance}emit(t,...e){return super.emit(t,...e)}on(t,e,s){return super.on(t,e,s)}off(t,e,s){return super.off(t,e,s)}};r(U,"instance");let Se=U;const l=Se.getInstance(),mt={menu:"assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Main_theme_exclusion_zone_looped.mp3?v=20260731td2assets2",world01:"assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Main_theme_Halloween_looped.mp3?v=20260731td2assets2",world02:"assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Main_theme_Chinese_Street.mp3?v=20260731td2assets2",world03:"assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Calm_theme_Chinese_Street.mp3?v=20260731td2assets2",world04:"assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Main_theme_exclusion_zone_alternative_looped.mp3?v=20260731td2assets2",world05:"assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Alternative_theme_Chinese_Street.mp3?v=20260731td2assets2",world06:"assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Stealthy_theme_loopable.mp3?v=20260731td2assets2",endless:"assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Battle_theme_Chinese_Street.mp3?v=20260731td2assets2",bossA:"assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Battle_theme_exclusion_zone_looped.mp3?v=20260731td2assets2",bossB:"assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Battle_theme_Halloween_looped.mp3?v=20260731td2assets2"},ut="music-",ft=1500;function Ge(d){return`${ut}${d}`}function gt(d){if(d===7)return"endless";const t=["world01","world02","world03","world04","world05","world06"],e=(d-1)%t.length;return t[e]}class _t{constructor(t){r(this,"scene");r(this,"volume");r(this,"currentTrackId",null);r(this,"currentSound",null);r(this,"crossfadeTweens",[]);r(this,"_onSetVolume");this.scene=t,this.volume=_.settings.musicVolume,this._onSetVolume=e=>this.setVolume(e.volume),l.on(p.SET_MUSIC_VOLUME,this._onSetVolume,this)}play(t,e=ft){if(t===this.currentTrackId)return;const s=Ge(t);if(!this.isTrackLoaded(s)){console.warn(`[MusicController] Track "${t}" (key: ${s}) not loaded — skipping playback.`);return}const i=this.createSound(s);if(i){if(this.currentSound&&this.currentTrackId!==null)this.crossfade(this.currentSound,i,e);else{this.setWebAudioVolume(i,this.volume);try{i.play()}catch(o){console.warn(`[MusicController] Failed to play track "${t}":`,o);return}}this.currentTrackId=t,this.currentSound=i}}hardCut(t){if(t===this.currentTrackId)return;this.killCrossfadeTweens(),this.currentSound&&(this.currentSound.stop(),this.currentSound.destroy(),this.currentSound=null,this.currentTrackId=null);const e=Ge(t);if(!this.isTrackLoaded(e)){console.warn(`[MusicController] Track "${t}" (key: ${e}) not loaded — skipping playback.`);return}const s=this.createSound(e);if(s){this.setWebAudioVolume(s,this.volume);try{s.play()}catch(i){console.warn(`[MusicController] Failed to play track "${t}":`,i);return}this.currentTrackId=t,this.currentSound=s}}setVolume(t){this.volume=Math.max(0,Math.min(1,t)),this.currentSound&&this.setWebAudioVolume(this.currentSound,this.volume)}stop(){this.killCrossfadeTweens(),this.currentSound&&(this.currentSound.stop(),this.currentSound.destroy(),this.currentSound=null,this.currentTrackId=null)}destroy(){this.stop(),l.off(p.SET_MUSIC_VOLUME,this._onSetVolume,this)}isTrackLoaded(t){return this.scene.cache.audio.has(t)}createSound(t){try{return this.scene.sound.add(t,{loop:!0,volume:0})}catch(e){return console.warn(`[MusicController] Failed to create sound for key "${t}":`,e),null}}crossfade(t,e,s){this.killCrossfadeTweens(),this.setWebAudioVolume(e,0);try{e.play()}catch(a){console.warn("[MusicController] Failed to start new track during crossfade:",a);return}const i=this.scene.tweens.add({targets:t,volume:0,duration:s,ease:"Linear",onComplete:()=>{t.stop(),t.destroy()}}),o=this.scene.tweens.add({targets:e,volume:this.volume,duration:s,ease:"Linear"});this.crossfadeTweens=[i,o]}killCrossfadeTweens(){for(const t of this.crossfadeTweens)t.isPlaying()&&t.stop();this.crossfadeTweens=[]}setWebAudioVolume(t,e){t.volume=e}}class yt extends u.Scene{constructor(){super({key:"PreloadScene"})}preload(){const{width:t,height:e}=this.cameras.main;this.add.rectangle(t/2,e/2,400,30,3355443);const s=this.add.rectangle(t/2-198,e/2,4,26,4521864);this.add.text(t/2,e/2-40,"DaveAI TD2",{fontSize:"32px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(.5);const i=this.add.text(t/2,e/2+40,"0%",{fontSize:"16px",color:"#aaaaaa",fontFamily:"monospace"}).setOrigin(.5);this.load.on("progress",o=>{s.width=4+392*o,i.setText(`${Math.floor(o*100)}%`)}),this.loadUIAssets(),this.loadVFXAssets(),H.preload(this),he.preload(this),this.loadMusicTracks(),_.load()}create(){this.scene.start("MainMenuScene")}loadUIAssets(){this.load.image("world-01-bg","assets/tilesets/world-01/layers/main_bg.png?v=20260731td2assets2");for(let t=1;t<=10;t++)this.load.image(`road-tile-${t}`,`assets/tilesets/world-01/layers/road_${t}.png?v=20260731td2assets2`);this.load.image("hud-heart","assets/ui/td-gui/interface_game/heart.png?v=20260731td2assets2"),this.load.image("hud-star","assets/ui/td-gui/interface_game/star.png?v=20260731td2assets2"),this.load.image("hud-bar-bg","assets/ui/td-gui/interface_game/bg_bar.png?v=20260731td2assets2");for(let t=1;t<=4;t++)this.load.image(`hud-bar-${t}`,`assets/ui/td-gui/interface_game/bar_${t}.png?v=20260731td2assets2`);for(let t=1;t<=8;t++)this.load.image(`hud-ico-${t}`,`assets/ui/td-gui/interface_game/ico_${t}.png?v=20260731td2assets2`);for(let t=1;t<=7;t++){const e=String(t).padStart(2,"0");this.load.image(`health-bar-${t}`,`assets/ui/health-bars/health_bar-${e}.png?v=20260731td2assets2`)}}loadMusicTracks(){for(const[t,e]of Object.entries(mt))this.load.audio(`music-${t}`,e)}loadVFXAssets(){const t=[{key:"vfx-damage",folder:"damage",prefix:"1_effect_damage_",count:10,fileStart:0},{key:"vfx-fire",folder:"fire",prefix:"1_effect_fire_",count:19,fileStart:0},{key:"vfx-freeze",folder:"freeze",prefix:"1_effect_freeze_",count:16,fileStart:0},{key:"vfx-lightning",folder:"zip",prefix:"1_effect_zip_",count:14,fileStart:0},{key:"vfx-poison",folder:"rain",prefix:"1_effect_rain_",count:39,fileStart:1},{key:"vfx-stone",folder:"stone",prefix:"1_effect_stone_",count:18,fileStart:0},{key:"vfx-time",folder:"time",prefix:"1_effect_time_",count:14,fileStart:0},{key:"vfx-defense",folder:"def",prefix:"1_effect_def_",count:10,fileStart:0}];for(const e of t)for(let s=0;s{this.scene.scene.start(t,e)})}addScene(t,e){this.scene.scene.launch(t,e)}stopScene(t){this.scene.scene.stop(t)}}class wt extends u.Scene{constructor(){super({key:"MainMenuScene"});r(this,"sceneManager")}create(){this.sceneManager=new ee(this);const{width:e,height:s}=this.cameras.main;this.add.rectangle(e/2,s/2,e,s,1710638),this.add.text(e/2,s*.2,"DAVEAI TD2",{fontSize:"64px",color:"#ffdd44",fontFamily:"sans-serif",fontStyle:"bold",stroke:"#000000",strokeThickness:6}).setOrigin(.5),this.add.text(e/2,s*.3,"Tower Defense",{fontSize:"28px",color:"#aaaaff",fontFamily:"sans-serif"}).setOrigin(.5),this.createButton(e/2,s*.5,"PLAY",()=>{this.sceneManager.transitionTo("WorldSelectScene")}),this.createButton(e/2,s*.62,"ENDLESS MODE",()=>{this.sceneManager.transitionTo("EndlessScene",{worldId:7})}),this.createButton(e/2,s*.74,"SETTINGS",()=>{this.scene.launch("SettingsScene")}),this.cameras.main.fadeIn(300,0,0,0)}createButton(e,s,i,o){const a=this.add.rectangle(e,s,280,52,2250154,1).setInteractive({useHandCursor:!0});this.add.text(e,s,i,{fontSize:"22px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(.5),a.on("pointerover",()=>{a.setFillStyle(3373004)}),a.on("pointerout",()=>{a.setFillStyle(2250154)}),a.on("pointerup",o)}}const vt=[{id:1,key:"world-01",name:"W1: Cursed Forest",color:2250018,lockedColor:3355443},{id:2,key:"world-02",name:"W2: Dusty Badlands",color:6702114,lockedColor:3355443},{id:3,key:"world-03",name:"W3: Frozen Wastes",color:2245734,lockedColor:3355443},{id:4,key:"world-04",name:"W4: Volcanic Depths",color:6693410,lockedColor:3355443},{id:5,key:"world-05",name:"W5: Haunted Marshes",color:3359795,lockedColor:3355443},{id:6,key:"world-06",name:"W6: The Void Gate",color:2236996,lockedColor:3355443},{id:7,key:"world-07",name:"Endless Arena",color:5588002,lockedColor:3355443},{id:8,key:"world-08",name:"W8: Graveyard March",color:3351091,lockedColor:3355443},{id:9,key:"world-09",name:"W9: Sunken Depths",color:2245717,lockedColor:3355443},{id:10,key:"world-10",name:"W10: Steel Fortress",color:4473924,lockedColor:3355443},{id:11,key:"world-11",name:"W11: The Vault",color:4465220,lockedColor:3355443},{id:12,key:"world-12",name:"W12: Rampart Breach",color:5588019,lockedColor:3355443},{id:13,key:"world-13",name:"W13: Dark Tunnels",color:3351091,lockedColor:3355443},{id:14,key:"world-14",name:"W14: Crystal Caverns",color:2245734,lockedColor:3355443},{id:15,key:"world-15",name:"W15: Storm Summit",color:3359829,lockedColor:3355443},{id:16,key:"world-16",name:"W16: Infernal Gates",color:5579281,lockedColor:3355443},{id:17,key:"world-17",name:"W17: Arcane Sanctum",color:3351108,lockedColor:3355443},{id:18,key:"world-18",name:"W18: Cursed Sands",color:4469538,lockedColor:3355443},{id:19,key:"world-19",name:"W19: Nether Gate",color:2232627,lockedColor:3355443},{id:20,key:"world-20",name:"W20: The Fracture",color:4469606,lockedColor:3355443},{id:21,key:"world-21",name:"W21: Jungle Ruins",color:2245666,lockedColor:3355443},{id:22,key:"world-22",name:"W22: Ancient Temple",color:4469521,lockedColor:3355443},{id:23,key:"world-23",name:"W23: Phantom Spire",color:2241348,lockedColor:3355443},{id:24,key:"world-24",name:"W24: Time Vortex",color:2245700,lockedColor:3355443},{id:25,key:"world-25",name:"W25: Sunken Caves",color:1127270,lockedColor:3355443},{id:26,key:"world-26",name:"W26: Deep Abyss",color:1122884,lockedColor:3355443},{id:27,key:"world-27",name:"W27: Gravity's End",color:2232644,lockedColor:3355443},{id:28,key:"world-28",name:"W28: Chaos Nexus",color:4465169,lockedColor:3355443},{id:29,key:"world-29",name:"W29: Abyssal Rift",color:1114146,lockedColor:3355443},{id:30,key:"world-30",name:"W30: Surface Breach",color:2241365,lockedColor:3355443}];class Et extends u.Scene{constructor(){super({key:"WorldSelectScene"});r(this,"sceneManager")}create(){this.sceneManager=new ee(this);const{width:e,height:s}=this.cameras.main;this.add.rectangle(e/2,s/2,e,s,1118498),this.add.text(e/2,32,"SELECT WORLD",{fontSize:"30px",color:"#ffdd44",fontFamily:"sans-serif",fontStyle:"bold"}).setOrigin(.5);const i=this.add.text(55,32,"<- BACK",{fontSize:"18px",color:"#aaaaaa",fontFamily:"sans-serif"}).setOrigin(.5).setInteractive({useHandCursor:!0});i.on("pointerover",()=>i.setColor("#ffffff")),i.on("pointerout",()=>i.setColor("#aaaaaa")),i.on("pointerup",()=>this.sceneManager.transitionTo("MainMenuScene"));const o=6,a=185,n=88,c=12,m=12,f=o*(a+c)-c,g=(e-f)/2+a/2,y=80;vt.forEach((w,E)=>{const T=E%o,x=Math.floor(E/o),S=g+T*(a+c),R=y+x*(n+m),D=_.isWorldUnlocked(w.key),P=D?w.color:w.lockedColor,C=this.add.rectangle(S,R,a,n,P).setInteractive({useHandCursor:D}).setStrokeStyle(2,D?8947967:4473924);this.add.text(S,R-14,w.name,{fontSize:"13px",color:D?"#ffffff":"#666666",fontFamily:"sans-serif",fontStyle:"bold",wordWrap:{width:a-12},align:"center"}).setOrigin(.5),D?this.add.text(S,R+20,`World ${w.id}`,{fontSize:"11px",color:"#aaaacc",fontFamily:"monospace"}).setOrigin(.5):this.add.text(S,R+18,"🔒 LOCKED",{fontSize:"12px",color:"#666666",fontFamily:"sans-serif"}).setOrigin(.5),D&&(C.on("pointerover",()=>{C.setStrokeStyle(3,16768324),C.setFillStyle(u.Display.Color.ValueToColor(P).brighten(20).color)}),C.on("pointerout",()=>{C.setStrokeStyle(2,8947967),C.setFillStyle(P)}),C.on("pointerup",()=>{w.id===7?this.sceneManager.transitionTo("EndlessScene",{worldId:7}):this.sceneManager.transitionTo("LevelSelectScene",{worldId:w.id,worldKey:w.key})}))}),this.cameras.main.fadeIn(300,0,0,0)}}class St extends u.Scene{constructor(){super({key:"LevelSelectScene"});r(this,"sceneManager")}create(e){this.sceneManager=new ee(this);const{width:s,height:i}=this.cameras.main,{worldId:o,worldKey:a}=e;this.add.rectangle(s/2,i/2,s,i,1118498),this.add.text(s/2,40,`WORLD ${o} — SELECT LEVEL`,{fontSize:"32px",color:"#ffdd44",fontFamily:"sans-serif"}).setOrigin(.5),this.add.text(60,40,"<- BACK",{fontSize:"20px",color:"#aaaaaa",fontFamily:"sans-serif"}).setOrigin(.5).setInteractive({useHandCursor:!0}).on("pointerup",()=>this.sceneManager.transitionTo("WorldSelectScene"));const c=6,m=s/2-350,f=i/2;for(let g=1;g<=c;g++){const y=g===6,w=y?`${a}-boss`:`${a}-level-0${g}`,E=_.getLevelData(a,w),T=m+(g-1)*140,x=y?8921634:E.completed?2245666:2236996,S=this.add.rectangle(T,f,120,120,x).setInteractive({useHandCursor:!0}).setStrokeStyle(2,8947967),R=y?"BOSS":`LVL ${g}`;this.add.text(T,f-20,R,{fontSize:"18px",color:"#ffffff",fontFamily:"sans-serif",fontStyle:"bold"}).setOrigin(.5);const D=E.stars,P=3-E.stars,C="*".repeat(D)+"-".repeat(P);this.add.text(T,f+20,C,{fontSize:"16px",color:"#ffdd44",fontFamily:"monospace"}).setOrigin(.5),S.on("pointerup",()=>{this.sceneManager.transitionTo("GameScene",{worldId:o,worldKey:a,levelId:w,isBoss:y})})}this.cameras.main.fadeIn(300,0,0,0)}}class bt{constructor(t,e,s){r(this,"scene");r(this,"waves");r(this,"pathPoints");r(this,"currentWaveIndex",-1);r(this,"spawnTimers",[]);r(this,"enemiesAlive",0);r(this,"enemiesSpawned",0);r(this,"totalEnemiesInWave",0);r(this,"isComplete",!1);this.scene=t,this.waves=e,this.pathPoints=s,l.on(p.ENEMY_DIED,this.onEnemyDied,this),l.on(p.ENEMY_REACHED_END,this.onEnemyDied,this)}get totalWaves(){return this.waves.length}get currentWave(){return this.currentWaveIndex+1}get isAllWavesComplete(){return this.isComplete}getState(){return{currentWave:this.currentWave,totalWaves:this.totalWaves,isActive:this.currentWaveIndex>=0&&!this.isComplete,enemiesAlive:this.enemiesAlive,enemiesSpawned:this.enemiesSpawned,totalEnemiesInWave:this.totalEnemiesInWave}}startNextWave(){if(this.currentWaveIndex>=this.waves.length-1)return this.isComplete=!0,l.emit(p.ALL_WAVES_COMPLETED),!1;this.currentWaveIndex++,this.clearTimers();const t=this.waves[this.currentWaveIndex];this.enemiesSpawned=0,this.enemiesAlive=0,this.totalEnemiesInWave=t.spawnGroups.reduce((s,i)=>s+i.count,0),l.emit(p.WAVE_STARTED,{wave:this.currentWave,total:this.totalWaves,enemyCount:this.totalEnemiesInWave});let e=0;return t.spawnGroups.forEach(s=>{e+=s.delay??0,this.scheduleGroup(s,e),e+=s.count*s.interval}),!0}scheduleGroup(t,e){for(let s=0;s{this.spawnEnemy(t.enemy)});this.spawnTimers.push(o)}}spawnEnemy(t){this.enemiesSpawned++,this.enemiesAlive++,l.emit(p.ENEMY_SPAWN_REQUEST,{enemyId:t,pathPoints:this.pathPoints,waveNumber:this.currentWave})}onEnemyDied(){this.enemiesAlive=Math.max(0,this.enemiesAlive-1),this.enemiesSpawned>=this.totalEnemiesInWave&&this.enemiesAlive===0&&this.onWaveComplete()}onWaveComplete(){const t=this.waves[this.currentWaveIndex];l.emit(p.WAVE_COMPLETED,{wave:this.currentWave,reward:t.reward})}pause(){this.spawnTimers.forEach(t=>t.paused=!0)}resume(){this.spawnTimers.forEach(t=>t.paused=!1)}clearTimers(){this.spawnTimers.forEach(t=>t.destroy()),this.spawnTimers=[]}destroy(){this.clearTimers(),l.off(p.ENEMY_DIED,this.onEnemyDied,this),l.off(p.ENEMY_REACHED_END,this.onEnemyDied,this)}}class X{static extractPathFromMap(t){const e=t.getObjectLayer("Path");if(!e)return console.warn('[PathFinder] No "Path" object layer found in map. Using fallback.'),X.getFallbackPath(t);const s=e.objects[0];if(!s)return X.getFallbackPath(t);const i=s.x??0,o=s.y??0,a=s.polyline??[];return a.length===0?X.getFallbackPath(t):a.map(n=>new u.Math.Vector2(i+n.x,o+n.y))}static getFallbackPath(t){const e=t.widthInPixels,s=t.heightInPixels,i=s/2;return[new u.Math.Vector2(-50,i),new u.Math.Vector2(e*.25,i),new u.Math.Vector2(e*.25,s*.2),new u.Math.Vector2(e*.5,s*.2),new u.Math.Vector2(e*.5,s*.8),new u.Math.Vector2(e*.75,s*.8),new u.Math.Vector2(e*.75,i),new u.Math.Vector2(e+50,i)]}static getPathLength(t){let e=0;for(let s=1;s=i){const c=(i-o)/n;return new u.Math.Vector2(u.Math.Linear(t[a-1].x,t[a].x,c),u.Math.Linear(t[a-1].y,t[a].y,c))}o+=n}return t[t.length-1].clone()}}const Tt=[{id:"archer",name:"Archer Tower",description:"Fast-firing ranged tower. Excellent against fast ground & flying enemies.",spriteKey:"tower-archer",frameRanges:{idle:[1,4],attack:[5,22],upgrade:[23,38],special:[39,76]},cost:100,targets:["ground","flying"],levels:[{level:1,damage:18,fireRate:1.2,range:180,projectile:"arrow",frameRange:[1,22]},{level:2,damage:32,fireRate:1.5,range:200,upgradeCost:120,projectile:"arrow",frameRange:[23,38]},{level:3,upgradeCost:150,paths:{marksman:{name:"Marksman",damage:80,fireRate:.8,range:260,projectile:"piercing-arrow",ability:"pierce",frameRange:[39,56]},"rapid-fire":{name:"Rapid Fire",damage:28,fireRate:3.5,range:180,projectile:"arrow",ability:"multi-shot",frameRange:[57,76]}}}]},{id:"magic",name:"Magic Tower",description:"Slow-firing but high damage. Effective against armored enemies. Can slow targets.",spriteKey:"tower-magic",frameRanges:{idle:[1,8],attack:[9,24],special:[25,32]},cost:150,targets:["ground","flying"],levels:[{level:1,damage:45,fireRate:.6,range:160,projectile:"magic-bolt",ignoresArmor:10,frameRange:[1,16]},{level:2,damage:80,fireRate:.75,range:180,upgradeCost:175,projectile:"magic-bolt",ignoresArmor:18,effect:"slow-20",frameRange:[17,24]},{level:3,upgradeCost:200,paths:{"arcane-storm":{name:"Arcane Storm",damage:120,fireRate:.5,range:200,projectile:"arcane-storm",ability:"chain-lightning",ignoresArmor:30,frameRange:[25,28]},"frost-mage":{name:"Frost Mage",damage:60,fireRate:.7,range:190,projectile:"frost-bolt",ability:"freeze",effect:"freeze-2s",ignoresArmor:20,frameRange:[29,32]}}}]},{id:"stone",name:"Stone Tower",description:"Heavy area damage. Crushes groups with boulders. Best against large crowds.",spriteKey:"tower-stone",frameRanges:{idle:[1,10],attack:[11,42],special:[43,61]},cost:125,targets:["ground"],levels:[{level:1,damage:35,fireRate:.5,range:140,projectile:"boulder",splashRadius:60,frameRange:[1,28]},{level:2,damage:60,fireRate:.6,range:155,upgradeCost:140,projectile:"boulder",splashRadius:80,effect:"stun-0.5",frameRange:[29,42]},{level:3,upgradeCost:175,paths:{catapult:{name:"Siege Catapult",damage:120,fireRate:.4,range:220,projectile:"catapult-shot",splashRadius:120,ability:"barrage",frameRange:[43,52]},crusher:{name:"Earthquake",damage:80,fireRate:.5,range:140,projectile:"ground-slam",splashRadius:100,ability:"earthquake",effect:"slow-40",frameRange:[53,61]}}}]},{id:"support",name:"Support Tower",description:"Buffs adjacent towers. Provides utility: speed boost, gold income, healing.",spriteKey:"tower-support",frameRanges:{idle:[1,6],active:[7,14],special:[15,18]},cost:80,targets:[],levels:[{level:1,auraRadius:120,effect:"tower-speed-boost-15",frameRange:[1,9]},{level:2,auraRadius:150,upgradeCost:100,effect:"tower-speed-boost-25",bonus:"gold-per-wave-5",frameRange:[10,14]},{level:3,upgradeCost:130,paths:{"battle-drum":{name:"Battle Drum",auraRadius:200,effect:"tower-damage-boost-30",ability:"rage-burst",frameRange:[15,16]},"gold-vault":{name:"Gold Vault",auraRadius:150,effect:"tower-speed-boost-20",ability:"interest-income",bonus:"gold-per-wave-20",frameRange:[17,18]}}}]}],Be={towers:Tt},At=16737792,Ue=16777028;class be extends u.GameObjects.Arc{constructor(e){super(e,0,0,6,0,360,!1,Ue,1);r(this,"_damage",0);r(this,"_speed",280);r(this,"_target");r(this,"_splashRadius",0);r(this,"_ignoresArmor",0);r(this,"_piercing",!1);r(this,"_pierceCount",0);r(this,"_detonated",!1);this.setDepth(5),e.add.existing(this),this.setActive(!1).setVisible(!1)}fire(e,s,i,o,a=0,n=0,c=!1){this.x=e.x,this.y=e.y,this._damage=i,this._speed=o,this._target=s,this._splashRadius=a,this._ignoresArmor=n,this._piercing=c,this._pierceCount=0,this._detonated=!1;const m=a>0?At:Ue;this.setFillStyle(m);const f=a>0?8:5;this.setRadius(f),this.setActive(!0).setVisible(!0)}update(e){if(!this.active||this._detonated)return;if(!this._target||!this._target.active||this._target.hp<=0){this.despawn();return}const s=this._target.x,i=this._target.y,o=s-this.x,a=i-this.y,n=Math.sqrt(o*o+a*a),c=this._speed*e/1e3;n<=c+4?(this.x=s,this.y=i,this.detonate()):(this.x+=o/n*c,this.y+=a/n*c)}onHit(e){}onDetonate(e,s,i){}detonate(){this._detonated||(this._splashRadius>0?(this._detonated=!0,this.onDetonate(this.x,this.y,this._splashRadius),this.dealSplashDamage(),this.despawn()):this._piercing&&this._pierceCount===0?(this.dealDirectDamage(this._target),this._pierceCount++,this._detonated=!0,this.despawn()):(this._detonated=!0,this.dealDirectDamage(this._target),this.despawn()))}dealDirectDamage(e){!e.active||e.hp<=0||(e.takeDamage(this._damage,this._ignoresArmor),this.onHit(e))}dealSplashDamage(){l.emit(p.PROJECTILE_SPLASH,{x:this.x,y:this.y,radius:this._splashRadius,damage:this._damage,ignoresArmor:this._ignoresArmor})}despawn(){this.setActive(!1).setVisible(!1),this._target=void 0}}const Dt={archer:5614165,magic:5592575,stone:8947848,support:16755200},xt=new Map(Be.towers.map(d=>[d.id,d]));class oe extends u.GameObjects.Container{constructor(e,s,i,o){super(e,s,i);r(this,"towerId");r(this,"towerData");r(this,"currentLevel",1);r(this,"currentPath",null);r(this,"totalInvestment",0);r(this,"bodyRect");r(this,"bodySprite");r(this,"rangeCircle");r(this,"levelBadge");r(this,"cooldownElapsed",0);r(this,"projectiles",[]);this.towerId=o;const a=xt.get(o);if(!a)throw new Error(`[TowerSprite] Unknown tower id: "${o}"`);this.towerData=a,this.totalInvestment=a.cost,this.buildVisuals(),e.add.existing(this)}getRange(){return this.resolveStats().range}getDamage(){return this.resolveStats().damage}getFireRate(){return this.resolveStats().fireRate}getTowerId(){return this.towerId}getLevel(){return this.currentLevel}getPath(){return this.currentPath}getTotalInvestment(){return this.totalInvestment}showRange(){this.rangeCircle.setVisible(!0);const e=this.resolveStats().range;this.rangeCircle.setRadius(e)}hideRange(){this.rangeCircle.setVisible(!1)}upgrade(e){var a;if(this.currentLevel>=3)return!1;const s=this.currentLevel,i=this.towerData.levels[s];if(!i)return!1;if(this.currentLevel===2){if(!e||!((a=i.paths)!=null&&a[e]))return!1;this.currentPath=e}const o=i.upgradeCost??0;return this.totalInvestment+=o,this.currentLevel++,this.updateVisuals(),!0}getUpgradeCost(e){if(this.currentLevel>=3)return 0;const s=this.currentLevel,i=this.towerData.levels[s];return i?i.upgradeCost??0:0}getSellValue(){return Math.floor(this.totalInvestment*.6)}sell(){const e=Math.floor(this.totalInvestment*.6);return this.destroy(),e}update(e,s,i){if(this.towerData.targets.length===0)return;const o=this.resolveStats(),a=1e3/Math.max(o.fireRate,.01);if(this.cooldownElapsed+=s,this.cooldownElapsedc.active?!0:(c.destroy(),!1)))}destroy(e){this.projectiles.forEach(s=>{s.active&&s.destroy()}),this.projectiles=[],super.destroy(e)}buildVisuals(){const e=Dt[this.towerId]??13421772,s=this.resolveStats().range;this.rangeCircle=this.scene.add.arc(0,0,s,0,360,!1,16777215,.15),this.rangeCircle.setStrokeStyle(1,16777215,.4),this.rangeCircle.setVisible(!1),this.add(this.rangeCircle),this.bodyRect=this.scene.add.rectangle(0,0,48,48,e),this.add(this.bodyRect);const i=he.getFirstFrameKey(this.towerId),o=this.scene.textures.exists(i)?i:"__DEFAULT";this.bodySprite=this.scene.add.sprite(0,0,o),this.bodySprite.setDisplaySize(48,48),this.add(this.bodySprite),this.bodyRect.setVisible(!1);const a=he.getIdleAnimKey(this.towerId);this.scene.anims.exists(a)&&this.bodySprite.play(a),this.levelBadge=this.scene.add.text(16,-20,"L1",{fontSize:"10px",color:"#ffffff",backgroundColor:"#000000",padding:{x:2,y:1},fontFamily:"monospace"}).setOrigin(.5,.5),this.add(this.levelBadge),this.setSize(48,48),this.setInteractive()}updateVisuals(){const e=`L${this.currentLevel}`;this.levelBadge.setText(e);const s=this.resolveStats().range;this.rangeCircle.setRadius(s)}resolveStats(){var i;const e=this.towerData.levels;if(this.currentLevel<3){const o=e[this.currentLevel-1]??e[0];return{damage:o.damage??0,fireRate:o.fireRate??0,range:o.range??0,ignoresArmor:o.ignoresArmor??0,splashRadius:o.splashRadius??0,auraRadius:o.auraRadius??0}}const s=e[2];if(!s)return{damage:0,fireRate:0,range:0,ignoresArmor:0,splashRadius:0,auraRadius:0};if(this.currentPath&&((i=s.paths)!=null&&i[this.currentPath])){const o=s.paths[this.currentPath];return{damage:o.damage??0,fireRate:o.fireRate??0,range:o.range??0,ignoresArmor:o.ignoresArmor??0,splashRadius:o.splashRadius??0,auraRadius:o.auraRadius??0}}return{damage:0,fireRate:0,range:0,ignoresArmor:0,splashRadius:0,auraRadius:0}}findTarget(e,s){const i=s*s,o=this.x,a=this.y;let n=null,c=1/0;for(const m of e){if(!m.active||m.hp<=0)continue;const f=m.x-o,g=m.y-a,y=f*f+g*g;y<=i&&ya.destroy()});const n=new be(this.scene);n.fire(i,e,s.damage,320,s.splashRadius,s.ignoresArmor);const c=this.scene;c.projectileLayer&&c.projectileLayer.add(n),this.projectiles.push(n)}}class Pt extends oe{constructor(e,s,i){super(e,s,i,"archer");r(this,"prevCooldownElapsed",0)}update(e,s,i){const o=this.getFireRate(),a=o>0?1e3/o:1/0;if(this.prevCooldownElapsed+=s,super.update(e,s,i),this.prevCooldownElapsed>=a){this.prevCooldownElapsed=0;const n=this.getLevel(),c=this.getPath(),m=this.findNearestEnemy(i);if(!m)return;n===2?(this.fireFlanking(m,15),this.fireFlanking(m,-15)):n===3&&c==="marksman"&&this.firePiercing(m)}}findNearestEnemy(e){const s=this.getRange(),i=s*s;let o=null,a=1/0;for(const n of e){if(!n.active||n.hp<=0)continue;const c=n.x-this.x,m=n.y-this.y,f=c*c+m*m;f<=i&&f{const s=e.x-this.x,i=e.y-this.y,o=s*s+i*i,a=this.getRange()+e.radius;if(o>a*a)return;const n=this.getLevel(),c=this.getPath();n===3&&c==="frost-mage"?this.applyAoeStatus("freeze",0,2e3,e.x,e.y,e.radius):n>=2&&this.applyAoeStatus("slow",.4,2e3,e.x,e.y,e.radius)});l.on(p.PROJECTILE_SPLASH,this.onSplash,this)}update(e,s,i){this.lastEnemies=i;const o=this.getFireRate(),a=o>0?1e3/o:1/0;if(this.prevCooldownElapsed+=s,super.update(e,s,i),this.prevCooldownElapsed>=a){this.prevCooldownElapsed=0;const n=this.findNearestEnemy(i);if(!n)return;const c=this.getLevel(),m=this.getPath(),f=c===3&&m==="arcane-storm"?"arcane":"ice",g={x:n.x,y:n.y,element:f,radius:this.getRange()};l.emit(p.DAMAGE_DEALT,g),c===1&&this.applyStatusEffect(n,"slow",.4,2e3),c===2&&this.applyStatusEffect(n,"slow",.2,2e3)}}destroy(e){l.off(p.PROJECTILE_SPLASH,this.onSplash,this),super.destroy(e)}applyStatusEffect(e,s,i,o){l.emit("tower:status_effect",{target:e,effect:s,factor:i,durationMs:o})}applyAoeStatus(e,s,i,o,a,n){const c=n*n;for(const m of this.lastEnemies){if(!m.active||m.hp<=0)continue;const f=m.x-o,g=m.y-a;f*f+g*g<=c&&this.applyStatusEffect(m,e,s,i)}}findNearestEnemy(e){const s=this.getRange(),i=s*s;let o=null,a=1/0;for(const n of e){if(!n.active||n.hp<=0)continue;const c=n.x-this.x,m=n.y-this.y,f=c*c+m*m;f<=i&&f{const s=e.x-this.x,i=e.y-this.y,o=s*s+i*i,a=this.getRange()+e.radius;if(o>a*a)return;const n=this.getLevel(),c=this.getPath();if(n===3&&c==="crusher"){const m={x:e.x,y:e.y,element:"poison",radius:e.radius,lingersMs:3e3};l.emit(p.DAMAGE_DEALT,m),l.emit("tower:poison_cloud",{x:e.x,y:e.y,radius:e.radius,damage:e.damage*.25,durationMs:3e3})}else if(n===3&&c==="catapult")l.emit("tower:armored_bonus",{x:e.x,y:e.y,radius:e.radius,bonus:.5});else{const m={x:e.x,y:e.y,element:"stone",radius:e.radius};l.emit(p.DAMAGE_DEALT,m)}});l.on(p.PROJECTILE_SPLASH,this.onSplash,this)}update(e,s,i){const o=this.getFireRate(),a=o>0?1e3/o:1/0;if(this.prevCooldownElapsed+=s,super.update(e,s,i),this.prevCooldownElapsed>=a){this.prevCooldownElapsed=0;const n=this.getLevel(),c=this.getPath();if(n===3&&c==="catapult"){const m=this.findNearestEnemy(i);m&&l.emit("tower:barrage",{towerId:this.getTowerId(),towerX:this.x,towerY:this.y,targetX:m.x,targetY:m.y,damage:this.getDamage()})}}}destroy(e){l.off(p.PROJECTILE_SPLASH,this.onSplash,this),super.destroy(e)}findNearestEnemy(e){const s=this.getRange(),i=s*s;let o=null,a=1/0;for(const n of e){if(!n.active||n.hp<=0)continue;const c=n.x-this.x,m=n.y-this.y,f=c*c+m*m;f<=i&&f[d.id,d.cost]));class q{constructor(t){r(this,"scene");r(this,"towers",new Map);r(this,"pathCells",new Set);r(this,"selected",null);r(this,"onBgPointerDown");r(this,"slowedUntil",new Map);r(this,"frozenUntil",new Map);r(this,"onGorthakShockwave");r(this,"onZhalfrixFreeze");this.scene=t,this.buildPathCellSet(t.pathPoints),this.onBgPointerDown=e=>{this.deselectAll()},t.input.on("pointerdown",this.onBgPointerDown),this.onGorthakShockwave=this.handleGorthakShockwave.bind(this),this.onZhalfrixFreeze=this.handleZhalfrixFreeze.bind(this),this.setupListeners()}setupListeners(){l.on("boss:gorthak_shockwave",this.onGorthakShockwave),l.on("boss:zhalfrix_freeze",this.onZhalfrixFreeze)}handleGorthakShockwave(t){const{x:e,y:s,radius:i}=t,o=i*i,a=performance.now()+3e3;for(const n of this.towers.values()){const c=n.worldX-e,m=n.worldY-s;c*c+m*m<=o&&this.slowedUntil.set(n.id,a)}}handleZhalfrixFreeze(t){const{duration:e,x:s,y:i,radius:o}=t,a=performance.now()+e;for(const n of this.towers.values()){if(o!==void 0&&s!==void 0&&i!==void 0){const c=o*o,m=n.worldX-s,f=n.worldY-i;if(m*m+f*f>c)continue}this.frozenUntil.set(n.id,a)}}canPlaceTower(t,e){const s=O(t,e);return!this.pathCells.has(s)&&!this.towers.has(s)}static worldToGrid(t,e){return{gridX:Math.floor(t/j),gridY:Math.floor((e-Ke)/j)}}static gridToWorld(t,e){return{worldX:t*j+j/2,worldY:Ke+e*j+j/2}}placeTower(t,e,s){if(!this.canPlaceTower(e,s))return null;const i=It.get(t)??0;if(_.gold{g.event.stopPropagation(),this.selectTower(m)}),l.emit(p.GOLD_CHANGED,-i),l.emit(p.TOWER_PLACED,{towerId:t,gridX:e,gridY:s,worldX:o,worldY:a,cost:i}),m}removeTower(t,e){const s=O(t,e),i=this.towers.get(s);i&&(this.selected===i&&this.deselectAll(),i.tower.destroy(),this.towers.delete(s))}removeTowerBySprite(t){for(const[e,s]of this.towers.entries())if(s.tower===t){this.selected===s&&(this.selected=null),this.towers.delete(e);return}}selectTower(t){this.selected!==t&&(this.deselectAll(),this.selected=t,t.tower.showRange(),l.emit(p.TOWER_SELECTED,{placed:t,level:t.tower.getLevel(),path:t.tower.getPath(),stats:{damage:t.tower.getDamage(),fireRate:t.tower.getFireRate(),range:t.tower.getRange()}}))}deselectAll(){this.selected&&(this.selected.tower.hideRange(),this.selected=null,l.emit(p.TOWER_DESELECTED))}getSelected(){return this.selected}upgradeSelected(t){if(!this.selected)return!1;const e=this.selected,s=e.tower.getUpgradeCost(t);return _.gold0?this.pool.pop():this.create();return t.setActive(!0).setVisible(!0),this.active.add(t),t}release(t){this.active.has(t)&&(this.active.delete(t),t.setActive(!1).setVisible(!1),this.pool.push(t))}releaseAll(){this.active.forEach(t=>this.release(t))}get activeCount(){return this.active.size}get poolSize(){return this.pool.length}create(){const t=this.factory();return t.setActive(!1).setVisible(!1),t}destroy(){this.active.forEach(t=>t.destroy()),this.pool.forEach(t=>t.destroy()),this.active.clear(),this.pool=[]}}const Ct=[{id:"enemy-01",name:"Goblin Warrior",pack:"A",world:1,hp:80,speed:90,armor:0,reward:8,size:"small",type:"ground",abilities:[]},{id:"enemy-02",name:"Goblin Archer",pack:"A",world:1,hp:60,speed:110,armor:0,reward:9,size:"small",type:"ground",abilities:["ranged"]},{id:"enemy-03",name:"Orc Brute",pack:"A",world:1,hp:200,speed:60,armor:10,reward:14,size:"large",type:"ground",abilities:[]},{id:"enemy-04",name:"Troll Guard",pack:"A",world:1,hp:320,speed:50,armor:15,reward:18,size:"large",type:"ground",abilities:["regen"]},{id:"enemy-05",name:"Skeleton Archer",pack:"A",world:2,hp:90,speed:100,armor:5,reward:11,size:"small",type:"ground",abilities:["ranged","undead"]},{id:"enemy-06",name:"Skeleton Warrior",pack:"A",world:2,hp:140,speed:80,armor:8,reward:12,size:"medium",type:"ground",abilities:["undead"]},{id:"enemy-07",name:"Demon Scout",pack:"A",world:2,hp:110,speed:130,armor:0,reward:13,size:"small",type:"ground",abilities:["fast"]},{id:"enemy-08",name:"Dark Elf",pack:"A",world:2,hp:160,speed:110,armor:5,reward:16,size:"medium",type:"ground",abilities:["stealth"]},{id:"enemy-09",name:"Gargoyle",pack:"A",world:2,hp:130,speed:120,armor:0,reward:17,size:"medium",type:"flying",abilities:["flying"]},{id:"enemy-10",name:"Night Stalker",pack:"A",world:2,hp:220,speed:95,armor:12,reward:22,size:"large",type:"ground",abilities:["stealth","fast"]},{id:"enemy-11",name:"Forest Sprite",pack:"B",world:3,hp:100,speed:115,armor:0,reward:12,size:"small",type:"ground",abilities:["regen"]},{id:"enemy-12",name:"Cave Worm",pack:"B",world:3,hp:280,speed:45,armor:20,reward:18,size:"large",type:"ground",abilities:["burrow"]},{id:"enemy-13",name:"Crystal Golem",pack:"B",world:3,hp:400,speed:40,armor:25,reward:22,size:"large",type:"ground",abilities:["magic_resist"]},{id:"enemy-14",name:"Ice Troll",pack:"B",world:3,hp:350,speed:55,armor:18,reward:20,size:"large",type:"ground",abilities:["freeze_aura"]},{id:"enemy-15",name:"Fire Imp",pack:"B",world:4,hp:120,speed:140,armor:0,reward:14,size:"small",type:"flying",abilities:["flying","fire_aura"]},{id:"enemy-16",name:"Stone Colossus",pack:"B",world:4,hp:600,speed:35,armor:35,reward:30,size:"large",type:"ground",abilities:["immune_slow"]},{id:"enemy-17",name:"Shadow Beast",pack:"B",world:4,hp:180,speed:125,armor:5,reward:19,size:"medium",type:"ground",abilities:["stealth","fast"]},{id:"enemy-18",name:"Bog Walker",pack:"B",world:4,hp:250,speed:70,armor:10,reward:17,size:"medium",type:"ground",abilities:["poison_trail"]},{id:"enemy-19",name:"Iron Golem",pack:"B",world:4,hp:500,speed:40,armor:30,reward:28,size:"large",type:"ground",abilities:["immune_slow","immune_freeze"]},{id:"enemy-20",name:"Chaos Daemon",pack:"B",world:4,hp:300,speed:100,armor:15,reward:25,size:"medium",type:"ground",abilities:["magic_resist","flying"]},{id:"enemy-21",name:"Swamp Toad",pack:"C",world:5,hp:200,speed:80,armor:5,reward:15,size:"medium",type:"ground",abilities:["poison"]},{id:"enemy-22",name:"Vine Creeper",pack:"C",world:5,hp:160,speed:65,armor:0,reward:13,size:"medium",type:"ground",abilities:["slow_aura"]},{id:"enemy-23",name:"Sand Scorpion",pack:"C",world:5,hp:240,speed:90,armor:15,reward:18,size:"medium",type:"ground",abilities:["poison","burrow"]},{id:"enemy-24",name:"Frost Wolf",pack:"C",world:5,hp:180,speed:130,armor:5,reward:16,size:"medium",type:"ground",abilities:["freeze_aura","fast"]},{id:"enemy-25",name:"Lava Drake",pack:"C",world:5,hp:350,speed:75,armor:20,reward:22,size:"large",type:"flying",abilities:["flying","fire_aura"]},{id:"enemy-26",name:"Necro Knight",pack:"C",world:5,hp:300,speed:80,armor:20,reward:24,size:"large",type:"ground",abilities:["undead","regen"]},{id:"enemy-27",name:"Blood Bat",pack:"C",world:5,hp:140,speed:150,armor:0,reward:15,size:"small",type:"flying",abilities:["flying","fast","lifesteal"]},{id:"enemy-28",name:"Ancient Mummy",pack:"C",world:6,hp:420,speed:55,armor:25,reward:28,size:"large",type:"ground",abilities:["undead","immune_slow","curse"]},{id:"enemy-29",name:"Wyvern Rider",pack:"C",world:6,hp:280,speed:110,armor:10,reward:26,size:"large",type:"flying",abilities:["flying","ranged"]},{id:"enemy-30",name:"Cursed Knight",pack:"C",world:6,hp:380,speed:85,armor:22,reward:28,size:"large",type:"ground",abilities:["undead","curse","regen"]},{id:"enemy-31",name:"Rat Raider",pack:"D",world:1,hp:50,speed:160,armor:0,reward:7,size:"tiny",type:"ground",abilities:["fast","swarm"]},{id:"enemy-32",name:"Crystal Spider",pack:"D",world:2,hp:110,speed:105,armor:10,reward:13,size:"small",type:"ground",abilities:["web_slow"]},{id:"enemy-33",name:"Mud Golem",pack:"D",world:3,hp:360,speed:45,armor:20,reward:20,size:"large",type:"ground",abilities:["immune_slow","regen"]},{id:"enemy-34",name:"Storm Hawk",pack:"D",world:4,hp:190,speed:145,armor:0,reward:19,size:"medium",type:"flying",abilities:["flying","fast"]},{id:"enemy-35",name:"Void Serpent",pack:"D",world:6,hp:340,speed:90,armor:15,reward:27,size:"large",type:"ground",abilities:["magic_resist","poison"]},{id:"enemy-36",name:"Chaos Mage",pack:"D",world:6,hp:200,speed:95,armor:5,reward:25,size:"medium",type:"ground",abilities:["magic_resist","spell_shield"]},{id:"enemy-37",name:"Rune Guard",pack:"D",world:6,hp:460,speed:60,armor:28,reward:30,size:"large",type:"ground",abilities:["spell_shield","immune_slow"]},{id:"enemy-38",name:"Abyssal Fiend",pack:"D",world:6,hp:280,speed:115,armor:10,reward:26,size:"medium",type:"ground",abilities:["stealth","teleport"]},{id:"enemy-39",name:"Warp Demon",pack:"D",world:6,hp:320,speed:120,armor:8,reward:28,size:"medium",type:"ground",abilities:["teleport","magic_resist"]},{id:"enemy-40",name:"Eldritch Horror",pack:"D",world:6,hp:700,speed:65,armor:30,reward:40,size:"large",type:"ground",abilities:["immune_slow","magic_resist","curse","regen"]},{id:"enemy-41",name:"Void Crawler",pack:"E",world:8,hp:150,speed:145,armor:0,reward:16,size:"small",type:"ground",abilities:["fast","undead","swarm"]},{id:"enemy-42",name:"Bone Reaper",pack:"E",world:8,hp:380,speed:70,armor:28,reward:22,size:"large",type:"ground",abilities:["undead","armor"]},{id:"enemy-43",name:"Plague Walker",pack:"E",world:8,hp:220,speed:75,armor:5,reward:20,size:"medium",type:"ground",abilities:["undead","heal_nearby"]},{id:"enemy-44",name:"Rot Hulk",pack:"E",world:8,hp:900,speed:40,armor:35,reward:35,size:"large",type:"ground",abilities:["undead","immune_slow"]},{id:"enemy-45",name:"Death Wisp",pack:"E",world:8,hp:160,speed:135,armor:0,reward:18,size:"small",type:"flying",abilities:["flying","undead","bypass_ground"]},{id:"enemy-46",name:"Grave Titan",pack:"E",world:9,hp:1200,speed:50,armor:40,reward:45,size:"large",type:"ground",abilities:["undead","boss_hp","regen"]},{id:"enemy-47",name:"Crypt Fiend",pack:"E",world:9,hp:280,speed:90,armor:10,reward:24,size:"medium",type:"ground",abilities:["undead","burrow","immune_while_burrowed"]},{id:"enemy-48",name:"Soul Leech",pack:"E",world:9,hp:240,speed:100,armor:5,reward:22,size:"medium",type:"ground",abilities:["undead","drain_tower"]},{id:"enemy-49",name:"Lich Knight",pack:"E",world:10,hp:450,speed:80,armor:20,reward:32,size:"large",type:"ground",abilities:["undead","revive","magic_resist"]},{id:"enemy-50",name:"Abomination",pack:"E",world:10,hp:600,speed:60,armor:25,reward:38,size:"large",type:"ground",abilities:["undead","split_on_death"]},{id:"enemy-51",name:"Feral Beast",pack:"F",world:9,hp:300,speed:125,armor:5,reward:22,size:"medium",type:"ground",abilities:["fast","rage"]},{id:"enemy-52",name:"Stone Goliath",pack:"F",world:9,hp:700,speed:40,armor:40,reward:35,size:"large",type:"ground",abilities:["immune_slow","immune_freeze"]},{id:"enemy-53",name:"Tide Runner",pack:"F",world:9,hp:200,speed:150,armor:0,reward:19,size:"small",type:"ground",abilities:["fast","amphibious"]},{id:"enemy-54",name:"Coral Serpent",pack:"F",world:9,hp:350,speed:85,armor:15,reward:25,size:"large",type:"ground",abilities:["poison","amphibious"]},{id:"enemy-55",name:"Mire Drake",pack:"F",world:10,hp:400,speed:90,armor:20,reward:28,size:"large",type:"flying",abilities:["flying","poison","fire_aura"]},{id:"enemy-56",name:"Furnace Guard",pack:"F",world:10,hp:550,speed:60,armor:30,reward:32,size:"large",type:"ground",abilities:["immune_freeze","armor","fire_resist"]},{id:"enemy-57",name:"Forge Titan",pack:"F",world:10,hp:850,speed:45,armor:45,reward:40,size:"large",type:"ground",abilities:["immune_slow","immune_freeze","shield"]},{id:"enemy-58",name:"Circuit Fiend",pack:"F",world:10,hp:350,speed:110,armor:10,reward:27,size:"medium",type:"ground",abilities:["electric","drain_tower","fast"]},{id:"enemy-59",name:"Entropy Wraith",pack:"F",world:11,hp:500,speed:100,armor:15,reward:36,size:"medium",type:"flying",abilities:["flying","magic_resist","phase"]},{id:"enemy-60",name:"Nexus Horror",pack:"F",world:11,hp:1e3,speed:70,armor:35,reward:55,size:"large",type:"ground",abilities:["magic_resist","immune_slow","regen","curse"]},{id:"enemy-61",name:"Shambling Zombie",pack:"G",world:8,hp:500,speed:40,armor:5,reward:14,size:"large",type:"ground",abilities:["undead","slow","very_high_hp"]},{id:"enemy-62",name:"Mummy Raider",pack:"G",world:8,hp:260,speed:75,armor:15,reward:20,size:"medium",type:"ground",abilities:["undead","curse","debuff_tower"]},{id:"enemy-63",name:"Ghoul Sprinter",pack:"G",world:8,hp:160,speed:155,armor:0,reward:16,size:"small",type:"ground",abilities:["undead","fast","swarm"]},{id:"enemy-64",name:"Wraith Mummy",pack:"G",world:8,hp:220,speed:120,armor:0,reward:22,size:"medium",type:"flying",abilities:["undead","flying","bypass_ground","curse"]},{id:"enemy-65",name:"Deep Lurker",pack:"H",world:9,hp:480,speed:65,armor:35,reward:28,size:"large",type:"ground",abilities:["armored","amphibious"]},{id:"enemy-66",name:"Tidal Spawn",pack:"H",world:9,hp:120,speed:150,armor:0,reward:14,size:"small",type:"ground",abilities:["fast","swarm","amphibious"]},{id:"enemy-67",name:"Abyssal Crawler",pack:"H",world:9,hp:1100,speed:35,armor:50,reward:48,size:"large",type:"ground",abilities:["immune_slow","armored","amphibious","massive_hp"]},{id:"enemy-68",name:"Brine Specter",pack:"H",world:9,hp:280,speed:130,armor:0,reward:26,size:"medium",type:"flying",abilities:["flying","amphibious","stealth","phase"]},{id:"enemy-69",name:"Kraken Hatchling",pack:"H",world:14,hp:600,speed:80,armor:20,reward:35,size:"large",type:"ground",abilities:["amphibious","regen","poison_trail"]},{id:"enemy-70",name:"Leviathan Shard",pack:"H",world:14,hp:1400,speed:50,armor:55,reward:60,size:"large",type:"ground",abilities:["amphibious","immune_slow","immune_freeze","massive_hp","regen"]},{id:"enemy-i01",name:"Star Shard",pack:"I",world:16,hp:120,speed:160,armor:0,reward:15,size:"small",type:"flying",abilities:["flying","fast","cosmic"]},{id:"enemy-i02",name:"Void Drone",pack:"I",world:16,hp:200,speed:130,armor:5,reward:20,size:"small",type:"flying",abilities:["flying","cosmic","electric"]},{id:"enemy-i03",name:"Nebula Crawler",pack:"I",world:16,hp:350,speed:95,armor:10,reward:26,size:"medium",type:"flying",abilities:["flying","cosmic","magic_resist"]},{id:"enemy-i04",name:"Orbital Sentinel",pack:"I",world:16,hp:480,speed:80,armor:20,reward:32,size:"medium",type:"flying",abilities:["flying","cosmic","shield","electric"]},{id:"enemy-i05",name:"Pulsar Wraith",pack:"I",world:16,hp:300,speed:120,armor:0,reward:28,size:"medium",type:"flying",abilities:["flying","cosmic","phase","stealth"]},{id:"enemy-i06",name:"Solar Construct",pack:"I",world:16,hp:700,speed:60,armor:35,reward:40,size:"large",type:"flying",abilities:["flying","cosmic","immune_freeze","fire_aura"]},{id:"enemy-i07",name:"Astral Titan",pack:"I",world:16,hp:1100,speed:45,armor:45,reward:52,size:"large",type:"flying",abilities:["flying","cosmic","immune_slow","magic_resist","boss_hp"]},{id:"enemy-i08",name:"Celestial Destroyer",pack:"I",world:16,hp:1600,speed:55,armor:50,reward:65,size:"large",type:"flying",abilities:["flying","cosmic","immune_slow","immune_freeze","magic_resist","regen"]},{id:"enemy-j01",name:"Scarab Scout",pack:"J",world:18,hp:80,speed:180,armor:0,reward:8,size:"tiny",type:"ground",abilities:["fast","swarm","burrow"]},{id:"enemy-j02",name:"Sand Raider",pack:"J",world:18,hp:200,speed:120,armor:5,reward:16,size:"small",type:"ground",abilities:["fast","desert"]},{id:"enemy-j03",name:"Dune Stalker",pack:"J",world:18,hp:340,speed:95,armor:12,reward:22,size:"medium",type:"ground",abilities:["desert","burrow","stealth"]},{id:"enemy-j04",name:"Arena Beast",pack:"J",world:18,hp:520,speed:80,armor:18,reward:30,size:"large",type:"ground",abilities:["desert","rage","immune_slow"]},{id:"enemy-j05",name:"Gladiator Construct",pack:"J",world:18,hp:450,speed:90,armor:25,reward:32,size:"large",type:"ground",abilities:["desert","armor","immune_freeze"]},{id:"enemy-j06",name:"Sandstorm Elemental",pack:"J",world:18,hp:600,speed:100,armor:15,reward:38,size:"large",type:"flying",abilities:["flying","desert","slow_aura","immune_slow"]},{id:"enemy-j07",name:"Ancient Guardian",pack:"J",world:19,hp:900,speed:65,armor:40,reward:48,size:"large",type:"ground",abilities:["desert","immune_slow","immune_freeze","regen","armor"]},{id:"enemy-j08",name:"Colosseum Champion",pack:"J",world:19,hp:1300,speed:55,armor:50,reward:58,size:"large",type:"ground",abilities:["desert","immune_slow","immune_freeze","immune_poison","magic_resist","boss_hp"]},{id:"enemy-k01",name:"Cave Crawler",pack:"K",world:21,hp:280,speed:110,armor:10,reward:18,size:"small",type:"ground",abilities:["fast","swarm","wall_cling"]},{id:"enemy-k02",name:"Rock Golem",pack:"K",world:21,hp:1800,speed:30,armor:60,reward:90,size:"large",type:"ground",abilities:["armored","immune_slow","immune_freeze"]},{id:"enemy-k03",name:"Blind Bat",pack:"K",world:21,hp:160,speed:130,armor:5,reward:14,size:"small",type:"flying",abilities:["flying","erratic","fast"]},{id:"enemy-k04",name:"Spore Walker",pack:"K",world:22,hp:400,speed:70,armor:15,reward:28,size:"medium",type:"ground",abilities:["spore_trail","poison_aura"]},{id:"enemy-k05",name:"Deep Lurker",pack:"K",world:23,hp:900,speed:55,armor:30,reward:55,size:"large",type:"ground",abilities:["burrow","stealth_emerge"]},{id:"enemy-k06",name:"Crystal Scorpion",pack:"K",world:24,hp:650,speed:75,armor:45,reward:42,size:"large",type:"ground",abilities:["armored","immune_slow","armor_break_on_death"]},{id:"enemy-k07",name:"Lava Toad",pack:"K",world:25,hp:500,speed:80,armor:20,reward:35,size:"medium",type:"ground",abilities:["fire_immune","splash_death","immune_burn"]},{id:"enemy-k08",name:"Void Worm",pack:"K",world:26,hp:1600,speed:45,armor:55,reward:85,size:"large",type:"ground",abilities:["phase_shift","elite","regen"]}],Mt={enemies:Ct},Ye={tiny:{w:16,h:16,color:16720418,yOffset:0},small:{w:20,h:20,color:16729088,yOffset:0},medium:{w:24,h:24,color:16746496,yOffset:0},large:{w:32,h:32,color:13386752,yOffset:0},flying:{w:20,h:20,color:16776960,yOffset:-20},boss:{w:48,h:48,color:8913032,yOffset:0}},Bt=32;class W extends u.GameObjects.Container{constructor(e,s,i){const o=i[0]??new u.Math.Vector2(0,0);super(e,o.x,o.y);r(this,"enemyId");r(this,"poolKey");r(this,"hp");r(this,"maxHp");r(this,"speed");r(this,"armor");r(this,"reward");r(this,"type");r(this,"pathPoints");r(this,"currentSegment");r(this,"distanceOnSegment");r(this,"totalProgress");r(this,"position");r(this,"slowFactor");r(this,"slowSources",[]);r(this,"frozenTime");r(this,"stunTime");r(this,"burnDps");r(this,"poisonDps");r(this,"poisonTime");r(this,"healthBarGfx");r(this,"bodyRect");r(this,"bodySprite");r(this,"spriteTypeIndex");r(this,"totalPathLength");r(this,"sizeCfg");r(this,"_released");r(this,"spawnPoint");r(this,"slowImmune",!1);r(this,"skipPathMovement",!1);this.enemyId=s,this.poolKey=s;const a=Mt.enemies.find(E=>E.id===s);if(!a)throw new Error(`[EnemyBase] Unknown enemyId: "${s}"`);this.maxHp=a.hp,this.hp=a.hp,this.speed=a.speed,this.armor=a.armor,this.reward=a.reward,this.type=a.type==="flying"?"flying":"ground";const n=this.type==="flying"?"flying":a.size??"medium";this.sizeCfg=Ye[n]??Ye.medium,this.pathPoints=i,this.currentSegment=0,this.distanceOnSegment=0,this.totalProgress=0,this.position=new u.Math.Vector2(o.x,o.y),this.spawnPoint=new u.Math.Vector2(o.x,o.y),this.totalPathLength=this._calcTotalLength(i),this.slowFactor=1,this.slowSources=[],this.frozenTime=0,this.stunTime=0,this.burnDps=0,this.poisonDps=0,this.poisonTime=0,this._released=!1;const{w:c,h:m,color:f,yOffset:g}=this.sizeCfg;this.bodyRect=e.add.rectangle(0,g,c,m,f),this.add(this.bodyRect),this.bodyRect.setVisible(!1),this.spriteTypeIndex=H.getSpriteTypeFromId(s);const y=`enemy-${this.spriteTypeIndex}-idle-000`,w=e.textures.exists(y)?y:"__DEFAULT";this.bodySprite=e.add.sprite(0,g,w),this.bodySprite.setDisplaySize(c*1.5,m*1.5),this.add(this.bodySprite),this.healthBarGfx=e.add.graphics(),this.add(this.healthBarGfx),this.setActive(!1).setVisible(!1),e.add.existing(this)}spawn(e){this.pathPoints=e,this.currentSegment=0,this.distanceOnSegment=0,this.totalProgress=0,this.hp=this.maxHp,this.slowFactor=1,this.slowSources=[],this.frozenTime=0,this.stunTime=0,this.burnDps=0,this.poisonDps=0,this.poisonTime=0,this._released=!1;const s=e[0]??new u.Math.Vector2(0,0);this.x=s.x,this.y=s.y,this.position.set(s.x,s.y),this.spawnPoint.set(s.x,s.y),this.setActive(!0).setVisible(!0);const i=H.getAnimKey(this.spriteTypeIndex,"walk");this.scene.anims.exists(i)&&this.bodySprite.play(i),this.updateHealthBar()}takeDamage(e,s=0){if(!this.active||this._released)return;const i=Math.max(0,this.armor-s),o=Math.max(1,e-i);this.hp-=o,l.emit(p.DAMAGE_DEALT,{damage:o,isCrit:o>=150,x:this.x,y:this.y}),this.updateHealthBar();const a=H.getAnimKey(this.spriteTypeIndex,"hurt");this.scene.anims.exists(a)?(this.bodySprite.play(a),this.bodySprite.once(u.Animations.Events.ANIMATION_COMPLETE,()=>{if(this.active&&!this._released){const n=H.getAnimKey(this.spriteTypeIndex,"walk");this.scene.anims.exists(n)&&this.bodySprite.play(n)}})):(this.bodySprite.setTint(16777215),this.scene.time.delayedCall(75,()=>{this.active&&!this._released&&this.bodySprite.clearTint()})),this.hp<=0&&this.onDeath()}applyEffect(e,s,i){if(!(!this.active||this._released))switch(l.emit("vfx:status-applied",{type:e,value:s,duration:i,x:this.x,y:this.y,enemy:this}),e){case"slow":if(this.slowImmune)break;this.slowSources.push(s),this.slowFactor=Math.min(...this.slowSources),this.scene.time.delayedCall(i,()=>{if(this.active&&!this._released){const o=this.slowSources.indexOf(s);o!==-1&&this.slowSources.splice(o,1),this.slowFactor=this.slowSources.length>0?Math.min(...this.slowSources):1,this.slowSources.length===0&&l.emit("vfx:status-expired",{type:"slow",enemy:this})}});break;case"freeze":this.frozenTime=Math.max(this.frozenTime,i);break;case"stun":this.stunTime=Math.max(this.stunTime,i);break;case"burn":this.burnDps=Math.max(this.burnDps,s),this.scene.time.delayedCall(i,()=>{this.active&&!this._released&&(this.burnDps=0,l.emit("vfx:status-expired",{type:"burn",enemy:this}))});break;case"poison":this.poisonDps=Math.max(this.poisonDps,s),this.poisonTime=Math.max(this.poisonTime,i),this.scene.time.delayedCall(i,()=>{this.active&&!this._released&&(this.poisonDps=0,this.poisonTime=0,l.emit("vfx:status-expired",{type:"poison",enemy:this}))});break}}update(e){if(!this.active||this._released)return;if(this.frozenTime>0){const o=this.frozenTime;if(this.frozenTime=Math.max(0,this.frozenTime-e),o>0&&this.frozenTime<=0&&l.emit("vfx:status-expired",{type:"freeze",enemy:this}),this.updateHealthBar(),this.frozenTime>0)return}if(this.stunTime>0){const o=this.stunTime;if(this.stunTime=Math.max(0,this.stunTime-e),o>0&&this.stunTime<=0&&l.emit("vfx:status-expired",{type:"stun",enemy:this}),this.updateHealthBar(),this.stunTime>0)return}if(this.burnDps>0&&(this.hp-=this.burnDps*e/1e3,this.hp<=0)){this.onDeath();return}if(this.poisonDps>0&&(this.hp-=this.poisonDps*e/1e3,this.hp<=0)){this.onDeath();return}if(this.onUpdate(e),this.skipPathMovement){this.updateHealthBar();return}if(this.currentSegment>=this.pathPoints.length-1){this.onReachEnd();return}let i=this.speed*this.slowFactor*e/1e3;for(;i>0&&this.currentSegment=c){if(i-=c,this.currentSegment++,this.distanceOnSegment=0,this.currentSegment>=this.pathPoints.length-1){const m=this.pathPoints[this.pathPoints.length-1];this.x=m.x,this.y=m.y,this.position.set(m.x,m.y),this.totalProgress=1,this.onReachEnd();return}}else this.distanceOnSegment+=i,i=0}if(this.currentSegment0?this.distanceOnSegment/n:0;this.x=u.Math.Linear(o.x,a.x,c),this.y=u.Math.Linear(o.y,a.y,c),this.position.set(this.x,this.y),this.bodySprite.setFlipX(a.x.5?52224:a>.25?16755200:16720384;if(this.healthBarGfx.fillStyle(n,1),this.healthBarGfx.fillRect(o,i,Math.ceil(e*a),s),this.frozenTime>0){const{w:c,h:m,yOffset:f}=this.sizeCfg;this.healthBarGfx.fillStyle(8965375,.35),this.healthBarGfx.fillRect(-(c/2),-(m/2)+f,c,m)}}_calcTotalLength(e){let s=0;for(let i=1;i.5&&this.bodySprite.setFlipX(o<0),this.totalFlightDist>0){const m=u.Math.Distance.BetweenPoints(this.spawnPoint,{x:this.x,y:this.y});this.totalProgress=u.Math.Clamp(m/this.totalFlightDist,0,1)}}}const pe=class pe extends W{constructor(t,e,s){super(t,e,s),this.armor=this.armor+pe.ARMOR_BONUS}};r(pe,"ARMOR_BONUS",15);let De=pe;const V=class V extends W{constructor(e,s,i){super(e,s,i);r(this,"siblings",[])}setSiblings(e){this.siblings=e}onUpdate(e){if(!this.active)return;const s=e/1e3,i=this.maxHp*V.SELF_REGEN_RATE*s;this.hp=Math.min(this.maxHp,this.hp+i);const o=V.NEARBY_RADIUS*V.NEARBY_RADIUS;for(const a of this.siblings){if(a===this||!a.active||a.hp<=0)continue;const n=a.x-this.x,c=a.y-this.y;if(n*n+c*c<=o){const f=a.maxHp*V.ALLY_REGEN_RATE*s;a.hp=Math.min(a.maxHp,a.hp+f)}}}};r(V,"SELF_REGEN_RATE",.02),r(V,"ALLY_REGEN_RATE",.01),r(V,"NEARBY_RADIUS",80);let ce=V;const F=class F extends W{constructor(t,e,s){super(t,e,s),this.maxHp=Math.round(this.maxHp*F.HP_MULTIPLIER),this.hp=this.maxHp,this.speed=this.speed*F.SPEED_MULTIPLIER,this.armor=this.armor+F.ARMOR_BONUS}onDeath(){super.onDeath(),l.emit(p.ENEMY_DIED,{enemyId:this.enemyId,reward:this.reward,hp:this.maxHp,x:this.x,y:this.y,elite:!0})}};r(F,"HP_MULTIPLIER",2.2),r(F,"SPEED_MULTIPLIER",1.4),r(F,"ARMOR_BONUS",8);let xe=F;class Ve{constructor(t,e){r(this,"scene");r(this,"pathPoints");r(this,"pools",new Map);r(this,"activeEnemies",new Set);r(this,"onSpawnRequest",t=>{const e=t.pathPoints??this.pathPoints;e!==this.pathPoints&&(this.pathPoints=e);const{enemyId:s}=t;if(s.startsWith("boss:")){l.emit(p.BOSS_SPAWN_REQUEST,{bossId:s.slice(5),pathPoints:this.pathPoints});return}this.spawnEnemy(s)});r(this,"onEnemyDied",t=>{this.releaseByEnemyId(t.enemyId,t.x,t.y)});r(this,"onEnemyReachedEnd",()=>{});r(this,"onProjectileSplash",t=>{for(const e of this.activeEnemies){if(!e.active)continue;u.Math.Distance.Between(t.x,t.y,e.x,e.y)<=t.radius&&e.takeDamage(t.damage,t.ignoresArmor)}});this.scene=t,this.pathPoints=e}setupListeners(){l.on(p.ENEMY_SPAWN_REQUEST,this.onSpawnRequest,this),l.on(p.ENEMY_DIED,this.onEnemyDied,this),l.on(p.ENEMY_REACHED_END,this.onEnemyReachedEnd,this),l.on(p.PROJECTILE_SPLASH,this.onProjectileSplash,this)}spawnEnemy(t){const s=this.getOrCreatePool(t).get();return this.scene.enemyLayer.getAll().includes(s)||this.scene.enemyLayer.add(s),s.spawn(this.pathPoints),s instanceof ce&&s.setSiblings(Array.from(this.activeEnemies)),this.activeEnemies.add(s),l.emit(p.ENEMY_SPAWNED,{enemyId:t,x:s.x,y:s.y}),s}getActiveEnemies(){return Array.from(this.activeEnemies)}trackEnemy(t){this.activeEnemies.add(t)}getEnemiesByProgress(){return Array.from(this.activeEnemies).sort((t,e)=>e.totalProgress-t.totalProgress)}update(t,e){for(const s of this.activeEnemies)s.active&&s.update(e);for(const s of this.activeEnemies)s.active||this.activeEnemies.delete(s)}destroy(){l.off(p.ENEMY_SPAWN_REQUEST,this.onSpawnRequest,this),l.off(p.ENEMY_DIED,this.onEnemyDied,this),l.off(p.ENEMY_REACHED_END,this.onEnemyReachedEnd,this),l.off(p.PROJECTILE_SPLASH,this.onProjectileSplash,this);for(const t of this.pools.values())t.destroy();this.pools.clear(),this.activeEnemies.clear()}releaseByEnemyId(t,e,s){for(const i of this.activeEnemies)if(i.enemyId===t&&Math.abs(i.x-e)<2&&Math.abs(i.y-s)<2){this.releaseEnemy(i);return}for(const i of this.activeEnemies)if(i.enemyId===t&&!i.active){this.releaseEnemy(i);return}}releaseEnemy(t){this.activeEnemies.delete(t);const e=this.pools.get(t.poolKey);e&&e.release(t)}getOrCreatePool(t){if(!this.pools.has(t)){const e=this.scene,s=this.pathPoints,i=new kt(e,()=>{const o=Ve.createEnemy(e,t,s);return o.poolKey=t,o},5);this.pools.set(t,i)}return this.pools.get(t)}static createEnemy(t,e,s){const i=e.indexOf(":"),o=i!==-1?e.slice(0,i):"",a=i!==-1?e.slice(i+1):e;switch(o){case"fast":return new Te(t,a,s);case"tank":return new Ae(t,a,s);case"flying":return new Vt(t,a,s);case"armored":return new De(t,a,s);case"healer":return new ce(t,a,s);case"elite":return new xe(t,a,s);default:return new W(t,a,s)}}}const zt=[{id:"boss-01-gorthak",name:"Gorthak the Defiler",world:1,hp:2500,speed:55,armor:20,reward:200,spriteKey:"boss-01-gorthak",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["charge"],description:"Charges forward, knocking aside projectiles"},{phase:2,hpThreshold:.5,abilities:["charge","summon-minions"],summons:["enemy-01","enemy-02"],summonCount:5,description:"Enrages and begins summoning goblin warriors and archers"}]},{id:"boss-02-ssylvara",name:"Ssylvara the Venomous",world:2,hp:3200,speed:65,armor:25,reward:280,spriteKey:"boss-02-ssylvara",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["poison-nova"],description:"Emits poison cloud that damages and slows towers"},{phase:2,hpThreshold:.4,abilities:["poison-nova","tunnel"],description:"Tunnels underground briefly, reappearing mid-path"}]},{id:"boss-03-zhalfrix",name:"Zhalfrix the Frozen",world:3,hp:4e3,speed:50,armor:30,reward:350,spriteKey:"boss-03-zhalfrix",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["freeze-pulse"],description:"Pulses ice waves that freeze towers in range"},{phase:2,hpThreshold:.35,abilities:["freeze-pulse","ice-armor"],armorBonus:20,description:"Encases itself in ice armor, massively increasing armor"}]},{id:"boss-04-maldreth",name:"Maldreth the Lava Lord",world:4,hp:5e3,speed:45,armor:35,reward:450,spriteKey:"boss-04-maldreth",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["lava-pool","fire-aura"],description:"Leaves lava pools that damage towers. Fire aura burns nearby towers."},{phase:2,hpThreshold:.3,abilities:["lava-pool","fire-aura","summon-lava-spawn"],summons:["lava-spawn"],summonCount:8,description:"Erupts, spawning Lava Spawn minions that swarm towers"}],minions:[{id:"lava-spawn",name:"Lava Spawn",spriteKey:"boss-04-maldreth-minion-lava-spawn",hp:80,speed:120,armor:0,reward:5,type:"ground"}]},{id:"boss-05-toad-king",name:"The Toad King",world:8,hp:6e3,speed:50,armor:30,reward:500,spriteKey:"boss-05-toad-king",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["summon-zombies","tongue-grab"],summons:["enemy-61","enemy-63"],summonCount:6,description:"Summons zombie swarms; tongue grab stuns towers briefly"},{phase:2,hpThreshold:.5,abilities:["summon-zombies","leap-attack"],description:"Leaps to a random path segment, instantly moving forward"}]},{id:"boss-06-rhino-beetle",name:"Rhinoceros Beetle",world:9,hp:7500,speed:55,armor:40,reward:620,spriteKey:"boss-06-rhino-beetle",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["charge-attack"],description:"Charge attack damages towers directly in path"},{phase:2,hpThreshold:.45,abilities:["charge-attack","shell-armor"],armorBonus:25,description:"Becomes armored — takes 50% reduced damage from all sources"}]},{id:"boss-07-iron-troll",name:"Iron Troll",world:10,hp:9e3,speed:45,armor:35,reward:750,spriteKey:"boss-07-iron-troll",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["regenerate"],regenPerSecond:60,description:"Rapidly regenerates health — towers must maintain constant damage"},{phase:2,hpThreshold:.4,abilities:["regenerate","summon-pack-e"],summons:["enemy-41","enemy-42","enemy-44"],summonCount:4,description:"Calls Pack E undead to reinforce while regenerating"}]},{id:"boss-08-ancient-mech",name:"Ancient Mech",world:11,hp:12e3,speed:40,armor:50,reward:900,spriteKey:"boss-08-ancient-mech",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["shield"],description:"Shield phases — immune to all damage while shield is active. Destroy the shield!"},{phase:2,hpThreshold:.5,abilities:["beam-attack"],description:"Beam attack disables towers in line for 4 seconds"}]},{id:"boss-09-frost-ooze",name:"Frost Ooze",world:11,hp:8e3,speed:50,armor:20,reward:850,spriteKey:"boss-09-frost-ooze",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["split-on-damage"],description:"Splits into smaller oozes at 75%, 50%, and 25% HP"},{phase:2,hpThreshold:.25,abilities:["split-on-damage","freeze-all"],description:"Final split triggers a global tower freeze for 5 seconds"}]},{id:"boss-10-magic-bear",name:"Magic Bear",world:11,hp:1e4,speed:55,armor:25,reward:1e3,spriteKey:"boss-10-magic-bear",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["absorb-magic"],description:"Absorbs magic damage — heals instead of taking damage from magic towers"},{phase:2,hpThreshold:.5,abilities:["absorb-magic","reverse-targeting"],description:"Reverses tower targeting — towers attack the wrong end of the path"}]},{id:"boss-11-storm-titan",name:"Storm Titan",world:12,hp:14e3,speed:50,armor:45,reward:950,spriteKey:"boss-11-storm-titan",size:"boss",type:"flying",phases:[{phase:1,hpThreshold:1,abilities:["lightning-chain"],description:"Chains lightning between 3 towers, disabling them for 3 seconds each"},{phase:2,hpThreshold:.5,abilities:["lightning-chain","storm-shield","summon-circuit-fiends"],summons:["enemy-58","enemy-34"],summonCount:5,description:"Activates storm shield (50% damage reduction) and summons Circuit Fiends"},{phase:3,hpThreshold:.2,abilities:["lightning-chain","overcharge"],description:"Overcharges — moves 60% faster and discharges lightning every 2 seconds"}]},{id:"boss-12-dungeon-lord",name:"The Dungeon Lord",world:13,hp:16e3,speed:42,armor:55,reward:1100,spriteKey:"boss-12-dungeon-lord",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["darkness-pulse","summon-undead"],summons:["enemy-41","enemy-45"],summonCount:8,description:"Darkness pulse blinds towers (range -50%) and summons undead hordes"},{phase:2,hpThreshold:.55,abilities:["darkness-pulse","bone-cage","revive-minions"],description:"Cages 2 towers completely. Revives dead minions at 20% HP."},{phase:3,hpThreshold:.2,abilities:["lich-form","undying"],description:"Transforms into Lich — becomes immune to physical damage. Only magic towers can hurt it."}]},{id:"boss-13-kraken",name:"The Kraken",world:14,hp:18e3,speed:38,armor:40,reward:1250,spriteKey:"boss-13-kraken",size:"boss",type:"amphibious",phases:[{phase:1,hpThreshold:1,abilities:["tentacle-grab","ink-cloud"],description:"Tentacle grab destroys a random tower. Ink cloud reduces tower range in an area."},{phase:2,hpThreshold:.6,abilities:["tentacle-grab","summon-tide-spawn","whirlpool"],summons:["enemy-66","enemy-68"],summonCount:10,description:"Summons Tidal Spawn swarms and creates a whirlpool that pulls enemies toward it — giving them a shortcut"},{phase:3,hpThreshold:.25,abilities:["kraken-rage","tsunami"],description:"Enraged — double speed, triple tentacle attacks. Tsunami wave slows all towers for 8 seconds."}]},{id:"boss-14-world-ender",name:"The World Ender",world:15,hp:3e4,speed:45,armor:60,reward:2e3,spriteKey:"boss-14-world-ender",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["absorb-damage","curse-all-towers"],description:"Absorbs 30% of all damage dealt to it and heals. Curses all towers — reducing their damage by 25%."},{phase:2,hpThreshold:.65,abilities:["void-rift","summon-elite-pack"],summons:["enemy-40","enemy-60","enemy-67"],summonCount:3,description:"Opens void rifts that summon Eldritch Horrors, Nexus Horrors, and Abyssal Crawlers simultaneously."},{phase:3,hpThreshold:.35,abilities:["reality-break","immune-slow","immune-freeze","immune-magic"],description:"Reality Break — becomes immune to slow, freeze, AND magic damage. Only physical non-magic towers work."},{phase:4,hpThreshold:.1,abilities:["final-form","all-immunities","berserker"],description:"Final Form — triples speed, removes all immunities, takes full damage. An all-out race to finish it off."}]},{id:"boss-15-celestial-titan",name:"Celestial Titan",world:16,hp:18e3,speed:60,armor:50,reward:1400,spriteKey:"boss-15-celestial-titan",size:"boss",type:"flying",phases:[{phase:1,hpThreshold:1,abilities:["stellar-pulse","gravity-well"],description:"Stellar pulse blinds towers for 2 seconds. Gravity well pulls projectiles off-course."},{phase:2,hpThreshold:.6,abilities:["stellar-pulse","gravity-well","summon-star-shards"],summons:["enemy-i01","enemy-i02"],summonCount:6,description:"Summons Star Shards — small flying fragments that orbit the boss and block projectiles."},{phase:3,hpThreshold:.25,abilities:["nova-burst","immune-ground-towers"],description:"Nova burst damages all towers in range. Ascends — only aerial-capable towers can hit it."}]},{id:"boss-16-the-archivist",name:"The Archivist",world:17,hp:2e4,speed:48,armor:55,reward:1600,spriteKey:"boss-16-the-archivist",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["curse-tome","summon-animated-books"],summons:["enemy-41","enemy-45"],summonCount:5,description:"Hurls cursed tomes that silence towers. Summons animated book constructs to swarm."},{phase:2,hpThreshold:.55,abilities:["curse-tome","maze-shift","revive-all-minions"],description:"Reshuffles bookshelves — path reroutes mid-level. Revives all dead minions at 30% HP."},{phase:3,hpThreshold:.2,abilities:["forbidden-knowledge","immune-magic","time-rewind"],description:"Reads forbidden text — becomes magic-immune. Time Rewind resets its position to mid-path once."}]},{id:"boss-17-sand-colossus",name:"The Sand Colossus",world:18,hp:25e3,speed:40,armor:65,reward:1800,spriteKey:"boss-17-sand-colossus",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["sandstorm","summon-scarabs"],summons:["enemy-j01","enemy-j02"],summonCount:10,description:"Sandstorm reduces all tower range by 40%. Summons Scarab swarms that overwhelm by sheer number."},{phase:2,hpThreshold:.5,abilities:["sandstorm","burrow"],description:"Burrows underground — untargetable for 6 seconds, re-emerges halfway along the path."},{phase:3,hpThreshold:.2,abilities:["colossus-stomp","sand-shield","immune-slow"],armorBonus:30,description:"Stomp destroys all towers in a radius. Sand shield absorbs 50% of damage. Immune to slow."}],minions:[{id:"enemy-j01",name:"Scarab Swarm",spriteKey:"enemy-scarab-swarm",hp:60,speed:160,armor:0,reward:3,type:"ground"}]},{id:"boss-18-sugar-witch",name:"The Sugar Witch",world:19,hp:22e3,speed:70,armor:40,reward:1900,spriteKey:"boss-18-sugar-witch",size:"boss",type:"flying",phases:[{phase:1,hpThreshold:1,abilities:["candy-coat","summon-gumdrop-goons"],summons:["enemy-j03","enemy-j04"],summonCount:8,description:"Candy-coats herself — first 3000 damage is absorbed by candy shell. Summons Gumdrop Goons."},{phase:2,hpThreshold:.6,abilities:["sugar-rush","cotton-candy-cloud"],description:"Sugar rush triples her speed for 5 seconds. Cotton candy cloud slows all towers' fire rate."},{phase:3,hpThreshold:.25,abilities:["confection-overload","candy-clone","immune-physical"],description:"Splits into 3 candy clones — only the real one takes damage. Clones move at double speed. Immune to physical."}]},{id:"boss-19-nexus-core",name:"The Nexus Core",world:20,hp:5e4,speed:35,armor:70,reward:5e3,spriteKey:"boss-19-nexus-core",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["reality-echo","summon-all-packs"],summons:["enemy-01","enemy-21","enemy-41","enemy-57","enemy-i01"],summonCount:5,description:"Reality echoes — summons one enemy from every pack simultaneously. Towers fired last wave take 20% more damage."},{phase:2,hpThreshold:.7,abilities:["nexus-drain","tower-corruption"],description:"Nexus drain siphons gold from the player (50/sec). Tower corruption randomly reverses one tower's targeting for 10 seconds."},{phase:3,hpThreshold:.45,abilities:["dimension-collapse","immune-magic","immune-slow"],description:"Dimension collapse — all towers lose 30% range AND damage. Immune to magic and slow."},{phase:4,hpThreshold:.2,abilities:["core-overload","immune-physical","infinite-summons"],description:"Core overload — immune to physical now too. Only towers with BOTH physical+magic damage can hit it. Summons 2 elite enemies every 4 seconds."},{phase:5,hpThreshold:.05,abilities:["final-convergence","all-immunities-removed","berserker"],description:"Final Convergence — ALL immunities drop. Moves at 4x speed. One last desperate charge — defeat it before it reaches the end!"}]},{id:"boss-20-jungle-titan",name:"The Jungle Titan",world:21,hp:28e3,speed:40,armor:55,reward:2200,spriteKey:"boss-20-jungle-titan",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["vine-grasp","canopy-collapse"],description:"Vine grasp entangles and halves the fire rate of 2 random towers. Canopy collapse drops debris in an AoE, briefly silencing towers under it."},{phase:2,hpThreshold:.5,abilities:["vine-grasp","canopy-collapse","summon-canopy-crawlers"],summons:["enemy-k01"],summonCount:8,description:"Enrages — vine reach doubles, collapse AoE grows. Summons 8 Cave Crawlers from the canopy above."}]},{id:"boss-21-frost-warden",name:"The Frost Warden",world:22,hp:18e3,speed:45,armor:50,reward:1800,spriteKey:"boss-21-frost-warden",size:"boss",type:"ground",tagTeamPartner:"boss-21-blizzard-queen",enrageOnPartnerDeath:!0,phases:[{phase:1,hpThreshold:1,abilities:["freeze-lock"],description:"Channels an ice beam that freezes all towers in a straight line for 3 seconds."},{phase:2,hpThreshold:0,trigger:"partner-death",abilities:["freeze-lock","enrage-solo"],description:"Partner's death triggers berserker mode — speed ×2, freeze-lock beam doubles in width."}]},{id:"boss-21-blizzard-queen",name:"The Blizzard Queen",world:22,hp:18e3,speed:55,armor:35,reward:1800,spriteKey:"boss-21-blizzard-queen",size:"boss",type:"flying",tagTeamPartner:"boss-21-frost-warden",phases:[{phase:1,hpThreshold:1,abilities:["ice-storm-aoe"],description:"Ice storm AoE slows all towers within radius by 40% for 5 seconds."},{phase:2,hpThreshold:.5,abilities:["ice-storm-aoe","warden-shield"],description:"Erects a crystalline shield around the Frost Warden — absorbs 50% of damage dealt to it."}]},{id:"boss-22-siege-colossus",name:"The Siege Colossus",world:23,hp:35e3,speed:30,armor:65,reward:2600,spriteKey:"boss-22-siege-colossus",size:"boss",type:"ground",immunities:["slow"],phases:[{phase:1,hpThreshold:1,abilities:["wall-charge","mortar-barrage","immune-slow"],description:"Wall-charge instantly destroys one tower per charge. Mortar barrage rains AoE fire on tower clusters."},{phase:2,hpThreshold:.6,abilities:["wall-charge","mortar-barrage","immune-slow","reinforce-armor"],armorBonus:30,description:"Siege armor activates — gains +30 armor. Charge rate doubles."},{phase:3,hpThreshold:.25,abilities:["wall-charge","mortar-barrage","immune-slow","immune-freeze","berserker"],description:"Final siege — immune to slow and freeze. All abilities fire simultaneously. Unstoppable charge."}]},{id:"boss-23-crystal-leviathan",name:"The Crystal Leviathan",world:24,hp:32e3,speed:35,armor:60,reward:2800,spriteKey:"boss-23-crystal-leviathan",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["crystal-shield","shard-burst"],description:"Crystal shield reflects 25% of projectile damage back to the attacking tower. Shard burst fires crystal shards in a 360° AoE."},{phase:2,hpThreshold:.65,abilities:["crystal-shield","shard-burst","burrow"],description:"Burrows underground for 5 seconds — completely untargetable. Re-emerges at mid-path with full shields."},{phase:3,hpThreshold:.3,abilities:["shard-storm","immune-physical"],description:"Shield shatters in a shard storm, raining crystal spikes on all towers. Becomes immune to physical damage — magic only."}]},{id:"boss-24-tide-serpent",name:"The Tide Serpent",world:25,hp:22e3,speed:60,armor:40,reward:2e3,spriteKey:"boss-24-tide-serpent",size:"boss",type:"ground",tagTeamPartner:"boss-24-depth-caller",phases:[{phase:1,hpThreshold:1,abilities:["charge-sweep"],description:"Sweeping charge hits all towers along its path, knocking them offline for 2 seconds each."},{phase:2,hpThreshold:0,trigger:"partner-low-hp",triggerThreshold:.4,abilities:["charge-sweep","empowered-surge"],description:"When Depth-Caller drops to 40% HP, Serpent enters empowered surge — gains +50% speed and damage for 20 seconds."}]},{id:"boss-24-depth-caller",name:"The Depth-Caller",world:25,hp:22e3,speed:40,armor:50,reward:2e3,spriteKey:"boss-24-depth-caller",size:"boss",type:"ground",tagTeamPartner:"boss-24-tide-serpent",phases:[{phase:1,hpThreshold:1,abilities:["deep-summon"],summons:["enemy-k05"],summonCount:6,description:"Continuously summons Deep Lurkers from submerged tunnels every 20 seconds."},{phase:2,hpThreshold:.5,abilities:["deep-summon","empower-partner"],summons:["enemy-k05"],summonCount:6,description:"Empowers the Tide Serpent — partner triggers berserker mode when Depth-Caller reaches 40% HP."}]},{id:"boss-25-myconid-sovereign",name:"The Myconid Sovereign",world:26,hp:38e3,speed:42,armor:45,reward:3e3,spriteKey:"boss-25-myconid-sovereign",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["spore-cloud"],description:"Spore cloud blinds all towers within a large radius for 4 seconds — they cannot attack."},{phase:2,hpThreshold:.7,abilities:["spore-cloud","spore-trail"],description:"Leaves a persistent spore trail that deals poison damage to any tower it passes near."},{phase:3,hpThreshold:.4,abilities:["spore-cloud","spore-trail","clone-split"],description:"Splits into 2 identical clones — only the real one takes damage. Clones still use all abilities."},{phase:4,hpThreshold:.15,abilities:["myconid-fury","immune-poison","clone-merge"],description:"Clones merge back into one. Becomes immune to poison and activates all abilities simultaneously at double intensity."}]},{id:"boss-26-bone-general-A",name:"Bone General Karveth",world:27,hp:2e4,speed:55,armor:45,reward:1900,spriteKey:"boss-26-bone-general-A",size:"boss",type:"ground",tagTeamPartner:"boss-26-bone-general-B",enrageOnPartnerDeath:!1,phases:[{phase:1,hpThreshold:1,abilities:["charge-rush"],description:"Bone charge rush — instantly destroys any tower it impacts, then resumes path."},{phase:2,hpThreshold:0,trigger:"partner-low-hp",triggerThreshold:.3,abilities:["charge-rush","revive-partner"],description:"When General B drops to 30% HP, General A uses a one-time revive — restoring partner to 30% HP. Charge-rush cooldown halved."}]},{id:"boss-26-bone-general-B",name:"Bone General Ossara",world:27,hp:2e4,speed:40,armor:55,reward:1900,spriteKey:"boss-26-bone-general-B",size:"boss",type:"ground",tagTeamPartner:"boss-26-bone-general-A",phases:[{phase:1,hpThreshold:1,abilities:["summon-skeleton-army","bone-shield-A"],summons:["enemy-k02"],summonCount:5,description:"Summons Rock Golems as skeleton honor guard. Projects a bone shield onto General A, absorbing 40% of incoming damage."},{phase:2,hpThreshold:.5,abilities:["summon-skeleton-army","bone-shield-A","death-march"],summons:["enemy-k01","enemy-k02"],summonCount:6,description:"Death March activates — all summoned skeletons gain +50% movement speed. Shield transfers if General A dies."}]},{id:"boss-27-magma-drake",name:"The Magma Drake",world:28,hp:45e3,speed:35,armor:70,reward:3500,spriteKey:"boss-27-magma-drake",size:"boss",type:"ground",immunities:["fire","burn","slow","freeze"],weakness:"ice",phases:[{phase:1,hpThreshold:1,abilities:["lava-wave","fire-aura","immune-fire","immune-burn"],description:"Lava wave destroys tower build sites it passes through. Fire aura continuously burns all towers within 200px."},{phase:2,hpThreshold:.65,abilities:["lava-wave","firewall","immune-fire","immune-burn","immune-slow"],description:"Firewall seals sections of the path — towers behind the wall cannot shoot through it."},{phase:3,hpThreshold:.35,abilities:["lava-wave","firewall","immune-fire","immune-burn","immune-slow","immune-freeze","immune-magic"],description:"Thermal armor hardens — immune to fire, slow, freeze, AND magic. Only ice towers deal full damage now."},{phase:4,hpThreshold:.1,abilities:["lava-meltdown","berserker","all-immunities-removed"],description:"Thermal armor collapses in a lava meltdown — ALL immunities drop. Takes 2× damage from all sources. Final desperate charge."}]},{id:"boss-28-void-herald",name:"The Void Herald",world:29,hp:3e4,speed:45,armor:55,reward:2800,spriteKey:"boss-28-void-herald",size:"boss",type:"ground",tagTeamPartner:"boss-28-rift-walker",enrageOnPartnerDeath:!0,phases:[{phase:1,hpThreshold:1,abilities:["void-shriek"],description:"Void shriek partially phases the Herald — 50% of projectiles pass through without dealing damage."},{phase:2,hpThreshold:0,trigger:"partner-phase-sync",triggerThreshold:.5,abilities:["void-shriek","combined-void-field"],description:"When Rift Walker reaches 50% HP, both bosses synchronise phases — they must both be killed within the same 8-second damage window."}]},{id:"boss-28-rift-walker",name:"The Rift Walker",world:29,hp:3e4,speed:60,armor:45,reward:2800,spriteKey:"boss-28-rift-walker",size:"boss",type:"ground",tagTeamPartner:"boss-28-void-herald",enrageOnPartnerDeath:!0,phases:[{phase:1,hpThreshold:1,abilities:["dimension-hop"],description:"Teleports between random path segments — appears at a point further along the path, bypassing towers."},{phase:2,hpThreshold:.5,abilities:["dimension-hop","pull-partner-sync"],description:"Pulls the Void Herald into phase synchronisation — both bosses must now be killed in the same 8-second kill window."}]},{id:"boss-29-the-convergence",name:"The Convergence",world:30,hp:1e5,speed:25,armor:80,reward:1e4,spriteKey:"boss-29-the-convergence",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["reality-echo","summon-prior-bosses"],summons:["boss-20-jungle-titan","boss-22-siege-colossus","boss-25-myconid-sovereign"],summonHp:.5,description:"Reality echo summons shadow versions of prior world bosses at 50% HP as reinforcements."},{phase:2,hpThreshold:.8,abilities:["rotating-immunity"],immunityRotationSec:30,description:"Immunity rotation begins — switches between fire, ice, magic, and physical immunity every 30 seconds."},{phase:3,hpThreshold:.6,abilities:["rotating-immunity","path-collapse"],description:"Sections of the map begin collapsing — enemies take alternate routes, splitting tower coverage."},{phase:4,hpThreshold:.4,abilities:["rotating-immunity","path-collapse","convergence-drain"],goldDrainPerSec:100,description:"Convergence drain siphons 100 gold every 5 seconds directly from the player's reserves."},{phase:5,hpThreshold:.2,abilities:["all-immunities-off","berserker","convergence-surge"],description:"All immunities collapse simultaneously. Speed triples. One final convergence surge of every ability at once."},{phase:6,hpThreshold:.05,abilities:["final-collapse","summon-all-bosses"],summons:["boss-21-frost-warden","boss-21-blizzard-queen","boss-23-crystal-leviathan","boss-24-tide-serpent","boss-26-bone-general-A","boss-27-magma-drake","boss-28-void-herald"],summonHp:.1,description:"Final collapse — summons shadow versions of ALL remaining boss types at 10% HP. The true final stand. Everything at once."}]}],ot={bosses:zt};class at extends W{constructor(e,s,i){const o=ot.bosses.find(n=>n.id===s);if(!o)throw new Error(`[BossEnemy] Unknown bossId: "${s}"`);super(e,"enemy-01",i);r(this,"bossData");r(this,"currentPhaseIndex");r(this,"nameLabel");this.bossData=o,this.enemyId=s,this.maxHp=o.hp,this.hp=o.hp,this.speed=o.speed,this.armor=o.armor,this.reward=o.reward,this.type=o.type==="flying"?"flying":"ground";const a=this.getAt(0);a&&"setSize"in a&&(a.setSize(48,48),a.setFillStyle(8913032)),this.nameLabel=e.add.text(0,-36,o.name,{fontSize:"10px",color:"#ffffff",fontFamily:"monospace",stroke:"#000000",strokeThickness:2}).setOrigin(.5,1),this.add(this.nameLabel),this.currentPhaseIndex=0,o.phases.length>0&&this.activatePhase(o.phases[0],!1)}spawn(e){super.spawn(e),this.currentPhaseIndex=0,this.bossData.phases.length>0&&this.activatePhase(this.bossData.phases[0],!1)}takeDamage(e,s=0){const i=this.hp;super.takeDamage(e,s),this.active&&i>0&&(this.checkPhaseTransitions(),this.active&&l.emit("boss:hp-update",{bossId:this.enemyId,hp:this.hp}))}checkPhaseTransitions(){const e=this.bossData.phases,s=this.hp/this.maxHp;for(let i=this.currentPhaseIndex+1;ii.startsWith("summon-"))&&e.summons&&e.summonCount){const i=e.summonCount,o=e.summons;for(let a=0;a=I.SHOCKWAVE_INTERVAL_MS&&(this.shockwaveCooldown=0,this.specialtyAttack()))}onPhase2Enter(){this.speed*=I.PHASE2_SPEED_MULT,this.damageMultiplier=I.PHASE2_DAMAGE_MULT,this.shockwaveCooldown=0}specialtyAttack(){l.emit("boss:gorthak_shockwave",{x:this.x,y:this.y,radius:I.SHOCKWAVE_RADIUS})}};r(I,"SHOCKWAVE_INTERVAL_MS",8e3),r(I,"SHOCKWAVE_RADIUS",150),r(I,"PHASE2_SPEED_MULT",1.5),r(I,"PHASE2_DAMAGE_MULT",2);let Pe=I;class Ft extends ue{constructor(t,e){super(t,"boss-02-ssylvara",e)}onPhase2Enter(){l.emit("boss:ssylvara_split",{x:this.x,y:this.y,hp:this.maxHp*.25}),this.setActive(!1).setVisible(!1)}specialtyAttack(){}}const J=class J extends ue{constructor(e,s){super(e,"boss-03-zhalfrix",s);r(this,"freezeCooldown",0)}update(e){super.update(e),this.phase===2&&this.active&&(this.freezeCooldown+=e,this.freezeCooldown>=J.FREEZE_INTERVAL_MS&&(this.freezeCooldown=0,this.specialtyAttack()))}onPhase2Enter(){this.freezeCooldown=0,this.specialtyAttack()}specialtyAttack(){l.emit("boss:zhalfrix_freeze",{duration:J.FREEZE_DURATION_MS})}};r(J,"FREEZE_INTERVAL_MS",12e3),r(J,"FREEZE_DURATION_MS",3e3);let Le=J;const Z=class Z extends ue{constructor(e,s){super(e,"boss-04-maldreth",s);r(this,"minionSpawnCooldown",0);r(this,"fireImmune",!1)}update(e){super.update(e),this.phase===2&&this.active&&(this.minionSpawnCooldown+=e,this.minionSpawnCooldown>=Z.SPAWN_INTERVAL_MS&&(this.minionSpawnCooldown=0,this.specialtyAttack()))}onPhase2Enter(){this.fireImmune=!0,this.minionSpawnCooldown=0,this.specialtyAttack()}specialtyAttack(){l.emit("boss:maldreth_spawn_minion",{x:this.x,y:this.y})}takeDamage(e,s=0,i){const o=this.fireImmune&&i==="fire"?e*(1-Z.FIRE_REDUCTION):e;super.takeDamage(o,s)}};r(Z,"SPAWN_INTERVAL_MS",15e3),r(Z,"FIRE_REDUCTION",.9);let Re=Z;class ze{constructor(t,e){r(this,"scene");r(this,"enemySystem");r(this,"activeBosses",[]);r(this,"onBossSpawnRequest",t=>{this.spawnBoss(t.bossId,t.pathPoints)});this.scene=t,this.enemySystem=e}setupListeners(){l.on(p.BOSS_SPAWN_REQUEST,this.onBossSpawnRequest,this)}spawnBoss(t,e){let s;try{s=ze.createBoss(this.scene,t,e)}catch(o){return console.error("[BossSystem] Failed to create boss:",o),null}this.scene.enemyLayer.add(s),s.spawn(e),this.activeBosses.push(s),this.enemySystem.trackEnemy(s);const i=ot.bosses.find(o=>o.id===t);return l.emit("boss:spawned",{bossId:t,bossName:(i==null?void 0:i.name)??t,maxHp:s.maxHp}),l.emit(p.ENEMY_SPAWNED,{enemyId:t,x:s.x,y:s.y}),s}update(t,e){for(const s of this.activeBosses)s.active&&s.update(e);for(const s of this.activeBosses)s.active||s.destroy();this.activeBosses=this.activeBosses.filter(s=>s.active)}destroy(){l.off(p.BOSS_SPAWN_REQUEST,this.onBossSpawnRequest,this);for(const t of this.activeBosses)t.destroy();this.activeBosses=[]}static createBoss(t,e,s){switch(e){case"boss-01-gorthak":return new Pe(t,s);case"boss-02-ssylvara":return new Ft(t,s);case"boss-03-zhalfrix":return new Le(t,s);case"boss-04-maldreth":return new Re(t,s);default:return new at(t,e,s)}}}const Xe=100,M=101,re=102,te={fontSize:"20px",color:"#ffffff",fontFamily:"monospace"};class Wt{constructor(t,e,s=150,i=20){r(this,"scene");r(this,"goldText");r(this,"livesText");r(this,"goldIcon");r(this,"livesIcon");r(this,"waveText");r(this,"waveBtn");r(this,"waveBtnText");r(this,"speedBtn");r(this,"speedMultiplier",1);r(this,"abilitySlots",[]);r(this,"abilityBar");r(this,"expBarBg");r(this,"expBarFill");r(this,"expBarWidth",0);r(this,"expBarY",0);r(this,"expLabel");this.scene=t,this.build(e,s,i)}updateGold(t){this.goldText.setText(this.goldIcon?String(t):`💰 ${t}`)}updateLives(t){this.livesText.setText(this.livesIcon?String(Math.max(0,t)):`❤️ ${Math.max(0,t)}`)}updateWave(t,e){this.waveText.setText(`Wave ${t}/${e}`)}setWaveButtonVisible(t){this.waveBtn.setVisible(t),this.waveBtnText.setVisible(t)}buildAbilityBar(t){var n;if((n=this.abilityBar)==null||n.destroy(),this.abilitySlots.forEach(c=>{c.bg.destroy(),c.label.destroy(),c.badge.destroy()}),this.abilitySlots=[],t.length===0)return;const{width:e}=this.scene.cameras.main,s=52,i=110,o=36,a=8;this.abilityBar=this.scene.add.graphics(),this.abilityBar.fillStyle(0,.65),this.abilityBar.fillRect(0,s,e,o+8),this.abilityBar.setScrollFactor(0).setDepth(100),t.forEach((c,m)=>{const f=a+m*(i+4),g=s+4,y=this.scene.add.rectangle(f+i/2,g+o/2,i,o,2241348).setScrollFactor(0).setDepth(101).setInteractive({useHandCursor:!0}).setStrokeStyle(1,4482730);y.on("pointerup",()=>{c.charges>0&&this.scene.events.emit("ui:use-ability",c.id)}),y.on("pointerover",()=>y.setFillStyle(3359829)),y.on("pointerout",()=>y.setFillStyle(c.charges>0?2241348:1118498));const w=this.scene.add.text(f+4,g+4,`${c.icon} ${c.name}`,{fontSize:"11px",color:"#aaddff",fontFamily:"monospace"}).setScrollFactor(0).setDepth(102),E=this.scene.add.text(f+i-4,g+o-4,`×${c.charges}`,{fontSize:"13px",color:c.charges>0?"#ffdd44":"#555555",fontFamily:"monospace",fontStyle:"bold"}).setOrigin(1,1).setScrollFactor(0).setDepth(102);this.abilitySlots.push({bg:y,label:w,badge:E})})}updateAbilityCharges(t,e){this.abilitySlots.forEach((s,i)=>{const o=e[i];if(!o)return;const a=t[o.id]??0;o.charges=a,s.badge.setText(`×${a}`),s.badge.setColor(a>0?"#ffdd44":"#555555"),s.bg.setFillStyle(a>0?2241348:1118498)})}buildExpBar(t){var i,o,a;(i=this.expBarBg)==null||i.destroy(),(o=this.expBarFill)==null||o.destroy(),(a=this.expLabel)==null||a.destroy();const{width:e}=this.scene.cameras.main;this.expBarWidth=e,this.expBarY=t?96:52;const s=4;this.expBarBg=this.scene.add.graphics(),this.expBarBg.fillStyle(1122867,.9),this.expBarBg.fillRect(0,this.expBarY,e,s),this.expBarBg.setScrollFactor(0).setDepth(Xe),this.expBarFill=this.scene.add.graphics(),this.expBarFill.setScrollFactor(0).setDepth(M),this._redrawExpFill(0),this.expLabel=this.scene.add.text(e-4,this.expBarY-1,"EXP",{fontSize:"9px",color:"#6699bb",fontFamily:"monospace"}).setOrigin(1,1).setScrollFactor(0).setDepth(M)}updateExpBar(t){this.expBarFill&&this._redrawExpFill(u.Math.Clamp(t,0,1))}destroy(){var t,e,s,i,o,a;(t=this.goldIcon)==null||t.destroy(),(e=this.livesIcon)==null||e.destroy(),(s=this.abilityBar)==null||s.destroy(),this.abilitySlots.forEach(n=>{n.bg.destroy(),n.label.destroy(),n.badge.destroy()}),(i=this.expBarBg)==null||i.destroy(),(o=this.expBarFill)==null||o.destroy(),(a=this.expLabel)==null||a.destroy()}build(t,e,s){const{width:i}=this.scene.cameras.main,o=this.scene.add.graphics();o.fillStyle(0,.75),o.fillRect(0,0,i,52),o.setScrollFactor(0).setDepth(Xe),this.scene.textures.exists("hud-ico-1")?(this.goldIcon=this.scene.add.image(28,26,"hud-ico-1").setDisplaySize(28,28).setScrollFactor(0).setDepth(M),this.goldText=this.scene.add.text(46,14,String(e),te).setScrollFactor(0).setDepth(M)):this.goldText=this.scene.add.text(16,14,`💰 ${e}`,te).setScrollFactor(0).setDepth(M),this.scene.textures.exists("hud-heart")?(this.livesIcon=this.scene.add.image(180,26,"hud-heart").setDisplaySize(28,28).setScrollFactor(0).setDepth(M),this.livesText=this.scene.add.text(198,14,String(s),te).setScrollFactor(0).setDepth(M)):this.livesText=this.scene.add.text(160,14,`❤️ ${s}`,te).setScrollFactor(0).setDepth(M),this.waveText=this.scene.add.text(i/2,14,`Wave 0/${t}`,te).setOrigin(.5,0).setScrollFactor(0).setDepth(M),this.speedBtn=this.scene.add.text(i-240,26,"⚡1×",{fontSize:"18px",color:"#ffff88",fontFamily:"monospace"}).setOrigin(.5).setScrollFactor(0).setDepth(re).setInteractive({useHandCursor:!0}),this.speedBtn.on("pointerup",()=>this.toggleSpeed()),this.speedBtn.on("pointerover",()=>this.speedBtn.setColor("#ffffff")),this.speedBtn.on("pointerout",()=>this.speedBtn.setColor("#ffff88"));const a=this.scene.add.text(i-185,26,"⏸",{fontSize:"22px",color:"#ffff88",fontFamily:"monospace"}).setOrigin(.5).setScrollFactor(0).setDepth(re).setInteractive({useHandCursor:!0});a.on("pointerup",()=>this.scene.events.emit("ui:toggle-pause")),a.on("pointerover",()=>a.setColor("#ffffff")),a.on("pointerout",()=>a.setColor("#ffff88")),this.waveBtn=this.scene.add.rectangle(i-90,26,160,38,2254370).setScrollFactor(0).setDepth(re).setInteractive({useHandCursor:!0}),this.waveBtnText=this.scene.add.text(i-90,26,"NEXT WAVE ▶",{fontSize:"14px",color:"#aaffaa",fontFamily:"monospace"}).setOrigin(.5).setScrollFactor(0).setDepth(re+1),this.waveBtn.on("pointerup",()=>this.scene.events.emit("ui:send-wave")),this.waveBtn.on("pointerover",()=>this.waveBtn.setFillStyle(3377203)),this.waveBtn.on("pointerout",()=>this.waveBtn.setFillStyle(2254370))}_redrawExpFill(t){if(!this.expBarFill)return;this.expBarFill.clear();const e=Math.floor(this.expBarWidth*t);if(e<=0)return;const s=4,i=Math.round(0+68*t),o=Math.round(170+85*t),n=i<<16|o<<8|255;this.expBarFill.fillStyle(n,1),this.expBarFill.fillRect(0,this.expBarY,e,s)}toggleSpeed(){this.speedMultiplier=this.speedMultiplier===1?2:1,this.speedBtn.setText(`⚡${this.speedMultiplier}×`),this.scene.events.emit("ui:speed-toggle",this.speedMultiplier)}}const ne=340,se=140,jt=200;class Nt{constructor(t,e){r(this,"scene");r(this,"towerSystem");r(this,"container",null);r(this,"onSelected");r(this,"onDeselected");this.scene=t,this.towerSystem=e,this.onSelected=s=>this.show(s),this.onDeselected=()=>this.hide(),l.on(p.TOWER_SELECTED,this.onSelected,this),l.on(p.TOWER_DESELECTED,this.onDeselected,this)}show(t){this.hide();const{width:e,height:s}=this.scene.cameras.main,i=e/2,o=s-se/2-10,a=[],n=this.scene.add.rectangle(0,0,ne,se,1118498,.92).setStrokeStyle(2,8947967);a.push(n);const c=t.placed.towerId.replace("tower-","").toUpperCase(),m=t.path?`Lv${t.level} (Path ${t.path})`:`Level ${t.level}`;a.push(this.scene.add.text(-ne/2+12,-se/2+10,`${c} — ${m}`,{fontSize:"16px",color:"#ffdd44",fontFamily:"monospace"}));const f=`DMG ${t.stats.damage} RATE ${t.stats.fireRate.toFixed(1)}/s RNG ${t.stats.range}`;a.push(this.scene.add.text(-ne/2+12,-se/2+32,f,{fontSize:"13px",color:"#aaaacc",fontFamily:"monospace"}));const g=t.placed.tower.getUpgradeCost(),y=g>0&&t.level<3,w=y?`UPGRADE 💰${g}`:"MAX LEVEL",E=this.scene.add.rectangle(-60,30,140,36,y?2245666:3355443).setStrokeStyle(1,y?4508740:5592405).setInteractive({useHandCursor:y});a.push(E);const T=this.scene.add.text(-60,30,w,{fontSize:"13px",color:y?"#88ff88":"#666666",fontFamily:"monospace"}).setOrigin(.5);a.push(T),y&&(E.on("pointerover",()=>E.setFillStyle(3368499)),E.on("pointerout",()=>E.setFillStyle(2245666)),E.on("pointerup",()=>{this.towerSystem.upgradeSelected();const P=this.towerSystem.getSelected();P?this.show({placed:P,level:P.tower.getLevel(),path:P.tower.getPath()??void 0,stats:{damage:P.tower.getDamage(),fireRate:P.tower.getFireRate(),range:P.tower.getRange()}}):this.hide()}));const x=t.placed.tower.getSellValue(),S=this.scene.add.rectangle(80,30,130,36,4465186).setStrokeStyle(1,13386820).setInteractive({useHandCursor:!0});a.push(S);const R=this.scene.add.text(80,30,`SELL 💰${x}`,{fontSize:"13px",color:"#ff8888",fontFamily:"monospace"}).setOrigin(.5);a.push(R),S.on("pointerover",()=>S.setFillStyle(6697779)),S.on("pointerout",()=>S.setFillStyle(4465186)),S.on("pointerup",()=>{this.towerSystem.sellSelected(),this.hide()});const D=this.scene.add.text(ne/2-14,-se/2+8,"✕",{fontSize:"16px",color:"#888888",fontFamily:"monospace"}).setOrigin(.5).setInteractive({useHandCursor:!0});a.push(D),D.on("pointerup",()=>{this.towerSystem.deselectAll(),this.hide()}),D.on("pointerover",()=>D.setColor("#ffffff")),D.on("pointerout",()=>D.setColor("#888888")),this.container=this.scene.add.container(i,o,a),this.container.setDepth(jt).setScrollFactor(0)}hide(){this.container&&(this.container.destroy(!0),this.container=null)}destroy(){l.off(p.TOWER_SELECTED,this.onSelected,this),l.off(p.TOWER_DESELECTED,this.onDeselected,this),this.hide()}}class Ht{constructor(t){r(this,"scene");r(this,"banner",null);this.scene=t}showNextWaveReady(t,e){this.showBanner(`⚔ Wave ${t} — Ready!`,`+💰${e} on completion`,2241314,2e3)}showWaveActive(t){this.showBanner(`⚔ Wave ${t} — INCOMING!`,"",3351057,1500)}showVictoryPending(){this.showBanner("🏆 All waves cleared!","Victory incoming…",2245666,2500)}destroy(){this.clearBanner()}showBanner(t,e,s,i){this.clearBanner();const{width:o}=this.scene.cameras.main,a=[],n=400,c=e?70:50,m=this.scene.add.rectangle(0,0,n,c,s,.88).setStrokeStyle(2,8947967);a.push(m),a.push(this.scene.add.text(0,e?-14:0,t,{fontSize:"20px",color:"#ffdd44",fontFamily:"monospace"}).setOrigin(.5)),e&&a.push(this.scene.add.text(0,14,e,{fontSize:"14px",color:"#aaffaa",fontFamily:"monospace"}).setOrigin(.5)),this.banner=this.scene.add.container(o/2,90,a),this.banner.setDepth(150).setScrollFactor(0).setAlpha(0),this.scene.tweens.add({targets:this.banner,alpha:1,duration:250,ease:"Power2",onComplete:()=>{this.scene.time.delayedCall(i,()=>{this.banner&&this.scene.tweens.add({targets:this.banner,alpha:0,duration:300,ease:"Power2",onComplete:()=>this.clearBanner()})})}})}clearBanner(){this.banner&&(this.banner.destroy(!0),this.banner=null)}}const A=class A{constructor(t){r(this,"container");r(this,"bg");r(this,"fill");r(this,"phaseMarker");r(this,"nameLabel");r(this,"hpLabel");r(this,"scene");r(this,"maxHp",100);r(this,"currentHp",100);this.scene=t,this.bg=t.add.rectangle(0,0,A.BAR_W+4,A.BAR_H+4,0,.85),this.fill=t.add.rectangle(-300,0,A.BAR_W,A.BAR_H,13378082,1).setOrigin(0,.5),this.phaseMarker=t.add.rectangle(0,0,3,A.BAR_H+6,16776960,1),this.nameLabel=t.add.text(0,-28,"BOSS",{fontSize:"14px",color:"#ffffff",fontStyle:"bold",fontFamily:"monospace"}).setOrigin(.5,1),this.hpLabel=t.add.text(A.BAR_W/2+8,0,"",{fontSize:"12px",color:"#ffcccc",fontFamily:"monospace"}).setOrigin(0,.5),this.container=t.add.container(A.BAR_X,A.BAR_Y,[this.bg,this.fill,this.phaseMarker,this.nameLabel,this.hpLabel]),this.container.setScrollFactor(0),this.container.setDepth(100),this.container.setVisible(!1),l.on(p.BOSS_PHASE_CHANGED,this.onPhaseChanged,this),l.on(p.BOSS_DIED,this.deactivate,this)}activate(t,e){this.maxHp=e,this.currentHp=e,this.nameLabel.setText(t.toUpperCase()),this.hpLabel.setText(`${e}`),this.fill.setFillStyle(13378082),this.fill.displayWidth=A.BAR_W,this.container.setVisible(!0)}setHP(t){this.currentHp=Math.max(0,t);const e=this.currentHp/this.maxHp;this.hpLabel.setText(`${Math.ceil(this.currentHp)}`),this.scene.tweens.add({targets:this.fill,displayWidth:A.BAR_W*e,duration:200,ease:"Linear"}),e<.3?this.fill.setFillStyle(16737792):e<.6?this.fill.setFillStyle(13386752):this.fill.setFillStyle(13378082),this.currentHp<=0&&this.deactivate()}deactivate(){this.container.setVisible(!1)}destroy(){l.off(p.BOSS_PHASE_CHANGED,this.onPhaseChanged,this),l.off(p.BOSS_DIED,this.deactivate,this),this.container.destroy()}onPhaseChanged(t){this.scene.tweens.add({targets:this.fill,alpha:{from:1,to:.3},yoyo:!0,repeat:3,duration:100})}};r(A,"BAR_W",600),r(A,"BAR_H",28),r(A,"BAR_X",640),r(A,"BAR_Y",24);let Oe=A;const _e=new Map(Be.towers.map(d=>[d.id,d])),B="monospace",N=2254370,qe=3377203,ie=4465186,Qe=5583667,Je=6702080,Gt=8939008,b=class b{constructor(t){r(this,"panel");r(this,"scene");r(this,"selectedTower",null);r(this,"titleText");r(this,"levelText");r(this,"upgradeBtnA");r(this,"upgradeBtnAText");r(this,"upgradeBtnB");r(this,"upgradeBtnBText");r(this,"sellBtn");r(this,"sellBtnText");r(this,"statDamage");r(this,"statRange");r(this,"statRate");r(this,"closeBtn");this.scene=t;const e=b.W,s=b.H,i=b.BTN_W,o=b.BTN_H,a=b.BTN_HLF,n=t.add.rectangle(0,0,e,s,1710638,.95).setStrokeStyle(2,4491519);this.titleText=t.add.text(0,-106,"TOWER",{fontSize:"14px",color:"#88aaff",fontStyle:"bold",fontFamily:B}).setOrigin(.5,0),this.levelText=t.add.text(0,-88,"Level 1",{fontSize:"11px",color:"#aaddff",fontFamily:B}).setOrigin(.5,0);const c=t.add.graphics();c.lineStyle(1,3359846,.8),c.lineBetween(-100,-68,e/2-10,-68);const m=-60;this.statDamage=t.add.text(-98,m,"DMG: —",{fontSize:"11px",color:"#ccddff",fontFamily:B}),this.statRange=t.add.text(-98,m+16,"RNG: —",{fontSize:"11px",color:"#ccddff",fontFamily:B}),this.statRate=t.add.text(-98,m+32,"SPD: —",{fontSize:"11px",color:"#ccddff",fontFamily:B});const f=-2;this.upgradeBtnA=t.add.rectangle(0,f,i,o,N).setStrokeStyle(1,4482730).setInteractive({useHandCursor:!0}),this.upgradeBtnAText=t.add.text(0,f,"UPGRADE 0g",{fontSize:"11px",color:"#aaffaa",fontFamily:B}).setOrigin(.5,.5),this.upgradeBtnA.on("pointerover",()=>{this.upgradeBtnA.setFillStyle(this.canAffordA()?qe:Qe)}),this.upgradeBtnA.on("pointerout",()=>{this.upgradeBtnA.setFillStyle(this.canAffordA()?N:ie)}),this.upgradeBtnA.on("pointerup",()=>this.onUpgradeA());const g=f+o+6;this.upgradeBtnB=t.add.rectangle(0,g,a*2,o,N).setStrokeStyle(1,4482730).setInteractive({useHandCursor:!0}).setVisible(!1),this.upgradeBtnBText=t.add.text(0,g,"",{fontSize:"11px",color:"#aaffaa",fontFamily:B}).setOrigin(.5,.5).setVisible(!1),this.upgradeBtnB.on("pointerover",()=>{this.upgradeBtnB.setFillStyle(this.canAffordB()?qe:Qe)}),this.upgradeBtnB.on("pointerout",()=>{this.upgradeBtnB.setFillStyle(this.canAffordB()?N:ie)}),this.upgradeBtnB.on("pointerup",()=>this.onUpgradeB());const y=82;this.sellBtn=t.add.rectangle(0,y,i,o,Je).setStrokeStyle(1,8939042).setInteractive({useHandCursor:!0}),this.sellBtnText=t.add.text(0,y,"SELL 0g",{fontSize:"11px",color:"#ffdd88",fontFamily:B}).setOrigin(.5,.5),this.sellBtn.on("pointerover",()=>this.sellBtn.setFillStyle(Gt)),this.sellBtn.on("pointerout",()=>this.sellBtn.setFillStyle(Je)),this.sellBtn.on("pointerup",()=>this.onSell()),this.closeBtn=t.add.text(e/2-6,-114,"✕",{fontSize:"12px",color:"#888888",fontFamily:B}).setOrigin(1,0).setInteractive({useHandCursor:!0}),this.closeBtn.on("pointerover",()=>this.closeBtn.setColor("#ffffff")),this.closeBtn.on("pointerout",()=>this.closeBtn.setColor("#888888")),this.closeBtn.on("pointerup",()=>this.close()),this.panel=t.add.container(0,0,[n,c,this.titleText,this.levelText,this.statDamage,this.statRange,this.statRate,this.upgradeBtnA,this.upgradeBtnAText,this.upgradeBtnB,this.upgradeBtnBText,this.sellBtn,this.sellBtnText,this.closeBtn]),this.panel.setScrollFactor(0),this.panel.setDepth(200),this.panel.setVisible(!1),l.on(p.GOLD_CHANGED,this.refreshAffordability,this)}open(t){this.selectedTower=t;const e=this.scene.cameras.main,s=t.x-e.scrollX,i=t.y-e.scrollY-130,o=u.Math.Clamp(s,b.W/2+10,1280-b.W/2-10),a=u.Math.Clamp(i,b.H/2+10,720-b.H/2-10);this.panel.setPosition(o,a),this.panel.setVisible(!0),this.refresh()}close(){this.selectedTower&&l.emit(p.TOWER_DESELECTED,{tower:this.selectedTower}),this.selectedTower=null,this.panel.setVisible(!1)}destroy(){l.off(p.GOLD_CHANGED,this.refreshAffordability,this),this.panel.destroy()}refresh(){const t=this.selectedTower;if(!t)return;const e=t.getLevel(),s=_e.get(t.getTowerId()),i=(s==null?void 0:s.name)??t.getTowerId().toUpperCase();this.titleText.setText(i.toUpperCase());const o=t.getPath()?` (${this.pathDisplayName(t.getTowerId(),t.getPath())})`:"";this.levelText.setText(`Level ${e}${o}`);const a=t.getDamage(),n=t.getRange(),c=t.getFireRate();this.statDamage.setText(`DMG: ${a>0?a:"—"}`),this.statRange.setText(`RNG: ${n}`),this.statRate.setText(`SPD: ${c>0?c.toFixed(1)+"/s":"—"}`);const m=t.getSellValue();if(this.sellBtnText.setText(`SELL ${m}g`),e>=3)this.setUpgradeAVisible(!1),this.upgradeBtnB.setVisible(!1),this.upgradeBtnBText.setVisible(!1);else if(e===2){const f=this.getL3Paths(t.getTowerId()),g=t.getUpgradeCost();f.length>=2?this.layoutTwoPathButtons(f[0],f[1],g):f.length===1?(this.layoutSingleUpgrade(`${f[0].name} ${g}g`,g),this.upgradeBtnB.setVisible(!1),this.upgradeBtnBText.setVisible(!1)):(this.layoutSingleUpgrade(`UPGRADE ${g}g`,g),this.upgradeBtnB.setVisible(!1),this.upgradeBtnBText.setVisible(!1))}else{const f=t.getUpgradeCost();this.layoutSingleUpgrade(`UPGRADE L2 ${f}g`,f),this.upgradeBtnB.setVisible(!1),this.upgradeBtnBText.setVisible(!1)}}refreshAffordability(t){this.selectedTower&&this.panel.visible&&this.refresh()}layoutSingleUpgrade(t,e){const s=_.gold>=e;this.upgradeBtnA.setSize(b.BTN_W,b.BTN_H),this.upgradeBtnA.setPosition(0,-2),this.upgradeBtnA.setFillStyle(s?N:ie),this.upgradeBtnAText.setPosition(0,-2),this.upgradeBtnAText.setText(t),this.upgradeBtnAText.setColor(s?"#aaffaa":"#ffaaaa"),this.setUpgradeAVisible(!0)}layoutTwoPathButtons(t,e,s){const i=_.gold>=s,o=b.BTN_HLF,a=b.BTN_H,n=-2;this.upgradeBtnA.setSize(o,a),this.upgradeBtnA.setPosition(-50,n),this.upgradeBtnA.setFillStyle(i?N:ie),this.upgradeBtnAText.setPosition(-50,n),this.upgradeBtnAText.setText(`${t.name} +${s}g`),this.upgradeBtnAText.setColor(i?"#aaffaa":"#ffaaaa"),this.setUpgradeAVisible(!0),this.upgradeBtnA.setData("pathKey",t.key),this.upgradeBtnB.setSize(o,a),this.upgradeBtnB.setPosition(o/2+3,n),this.upgradeBtnB.setFillStyle(i?N:ie),this.upgradeBtnBText.setPosition(o/2+3,n),this.upgradeBtnBText.setText(`${e.name} +${s}g`),this.upgradeBtnBText.setColor(i?"#aaffaa":"#ffaaaa"),this.upgradeBtnB.setData("pathKey",e.key),this.upgradeBtnB.setVisible(!0),this.upgradeBtnBText.setVisible(!0)}setUpgradeAVisible(t){this.upgradeBtnA.setVisible(t),this.upgradeBtnAText.setVisible(t)}onUpgradeA(){const t=this.selectedTower;if(!t)return;const e=t.getLevel();if(e>=3)return;const s=e===2?this.upgradeBtnA.getData("pathKey"):void 0;this.doUpgrade(t,s)}onUpgradeB(){const t=this.selectedTower;if(!t||t.getLevel()!==2)return;const e=this.upgradeBtnB.getData("pathKey");this.doUpgrade(t,e)}doUpgrade(t,e){const s=t.getUpgradeCost(e);_.gold({key:i,name:o.name})):[]}canAffordA(){const t=this.selectedTower;if(!t)return!1;const e=this.upgradeBtnA.getData("pathKey");return _.gold>=t.getUpgradeCost(e)}canAffordB(){const t=this.selectedTower;if(!t)return!1;const e=this.upgradeBtnB.getData("pathKey");return _.gold>=t.getUpgradeCost(e)}};r(b,"W",220),r(b,"H",240),r(b,"BTN_W",196),r(b,"BTN_H",28),r(b,"BTN_HLF",94);let Ie=b;class Ut{constructor(t){r(this,"circle");this.circle=t.add.graphics(),this.circle.setDepth(10),this.hide()}show(t,e,s,i=4491519){this.circle.clear(),this.circle.lineStyle(2,i,.8),this.circle.fillStyle(i,.15),this.circle.strokeCircle(t,e,s),this.circle.fillCircle(t,e,s),this.circle.setVisible(!0)}hide(){this.circle.clear(),this.circle.setVisible(!1)}destroy(){this.circle.destroy()}}const $t="modulepreload",Kt=function(d){return"/games/daveai-td2/"+d},Ze={},h=function(t,e,s){let i=Promise.resolve();if(e&&e.length>0){document.getElementsByTagName("link");const a=document.querySelector("meta[property=csp-nonce]"),n=(a==null?void 0:a.nonce)||(a==null?void 0:a.getAttribute("nonce"));i=Promise.allSettled(e.map(c=>{if(c=Kt(c),c in Ze)return;Ze[c]=!0;const m=c.endsWith(".css"),f=m?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${f}`))return;const g=document.createElement("link");if(g.rel=m?"stylesheet":$t,m||(g.as="script"),g.crossOrigin="",g.href=c,n&&g.setAttribute("nonce",n),document.head.appendChild(g),m)return new Promise((y,w)=>{g.addEventListener("load",y),g.addEventListener("error",()=>w(new Error(`Unable to preload CSS for ${c}`)))})}))}function o(a){const n=new Event("vite:preloadError",{cancelable:!0});if(n.payload=a,window.dispatchEvent(n),!n.defaultPrevented)throw a}return i.then(a=>{for(const n of a||[])n.status==="rejected"&&o(n.reason);return t().catch(o)})},Yt=Object.assign({"../data/levels/world-01/level-01.json":()=>h(()=>import("./level-01-C_tB77dN.js"),[]),"../data/levels/world-01/level-02.json":()=>h(()=>import("./level-02-UoZTcMHJ.js"),[]),"../data/levels/world-01/level-03.json":()=>h(()=>import("./level-03-BdPUuNv5.js"),[]),"../data/levels/world-01/level-04.json":()=>h(()=>import("./level-04-BiZwQXaQ.js"),[]),"../data/levels/world-01/level-05.json":()=>h(()=>import("./level-05-CXZoxkyz.js"),[]),"../data/levels/world-01/level-06.json":()=>h(()=>import("./level-06-DAWpXHVq.js"),[]),"../data/levels/world-01/level-07.json":()=>h(()=>import("./level-07-CHD1Xsit.js"),[]),"../data/levels/world-02/level-01.json":()=>h(()=>import("./level-01-BNR7SWIG.js"),[]),"../data/levels/world-02/level-02.json":()=>h(()=>import("./level-02-CFzKI2Bx.js"),[]),"../data/levels/world-02/level-03.json":()=>h(()=>import("./level-03-Cw1TA8c_.js"),[]),"../data/levels/world-02/level-04.json":()=>h(()=>import("./level-04-DaaRq2NI.js"),[]),"../data/levels/world-02/level-05.json":()=>h(()=>import("./level-05-B2cAbMs8.js"),[]),"../data/levels/world-02/level-06.json":()=>h(()=>import("./level-06-YTa0-aYV.js"),[]),"../data/levels/world-02/level-07.json":()=>h(()=>import("./level-07-BZ53SnmN.js"),[]),"../data/levels/world-03/level-01.json":()=>h(()=>import("./level-01-Cz_67A_L.js"),[]),"../data/levels/world-03/level-02.json":()=>h(()=>import("./level-02-BJ08Y0-C.js"),[]),"../data/levels/world-03/level-03.json":()=>h(()=>import("./level-03-CGOkzcdq.js"),[]),"../data/levels/world-03/level-04.json":()=>h(()=>import("./level-04-C2N4BMop.js"),[]),"../data/levels/world-03/level-05.json":()=>h(()=>import("./level-05-C5duy23o.js"),[]),"../data/levels/world-03/level-06.json":()=>h(()=>import("./level-06-CqfzP3Te.js"),[]),"../data/levels/world-03/level-07.json":()=>h(()=>import("./level-07-BDbSIpdr.js"),[]),"../data/levels/world-04/level-01.json":()=>h(()=>import("./level-01-KVMcvgTB.js"),[]),"../data/levels/world-04/level-02.json":()=>h(()=>import("./level-02-DS4drlZU.js"),[]),"../data/levels/world-04/level-03.json":()=>h(()=>import("./level-03-BkdV1vQf.js"),[]),"../data/levels/world-04/level-04.json":()=>h(()=>import("./level-04-ZR_BL7MG.js"),[]),"../data/levels/world-04/level-05.json":()=>h(()=>import("./level-05-l19sR8Li.js"),[]),"../data/levels/world-04/level-06.json":()=>h(()=>import("./level-06-HQNRzw41.js"),[]),"../data/levels/world-04/level-07.json":()=>h(()=>import("./level-07-DpvV5IZY.js"),[]),"../data/levels/world-05/level-01.json":()=>h(()=>import("./level-01-xDmMuLen.js"),[]),"../data/levels/world-05/level-02.json":()=>h(()=>import("./level-02-BeJR54Bw.js"),[]),"../data/levels/world-05/level-03.json":()=>h(()=>import("./level-03-CVHVRBEC.js"),[]),"../data/levels/world-05/level-04.json":()=>h(()=>import("./level-04-CkDJrV7j.js"),[]),"../data/levels/world-05/level-05.json":()=>h(()=>import("./level-05-CsTw6-2u.js"),[]),"../data/levels/world-05/level-06.json":()=>h(()=>import("./level-06-Bbg5vx6K.js"),[]),"../data/levels/world-05/level-07.json":()=>h(()=>import("./level-07-b7nO1vnm.js"),[]),"../data/levels/world-06/level-01.json":()=>h(()=>import("./level-01-DkwEvOWw.js"),[]),"../data/levels/world-06/level-02.json":()=>h(()=>import("./level-02-BiQu3Buu.js"),[]),"../data/levels/world-06/level-03.json":()=>h(()=>import("./level-03-e4XqrG8p.js"),[]),"../data/levels/world-06/level-04.json":()=>h(()=>import("./level-04-BEZWIobi.js"),[]),"../data/levels/world-06/level-05.json":()=>h(()=>import("./level-05-CTSRgiRY.js"),[]),"../data/levels/world-06/level-06.json":()=>h(()=>import("./level-06-B72nC4xc.js"),[]),"../data/levels/world-06/level-07.json":()=>h(()=>import("./level-07-DrzsViWh.js"),[]),"../data/levels/world-08/level-01.json":()=>h(()=>import("./level-01-D89GdeZM.js"),[]),"../data/levels/world-08/level-02.json":()=>h(()=>import("./level-02-Dqh6s7wo.js"),[]),"../data/levels/world-08/level-03.json":()=>h(()=>import("./level-03-M-6qFCIU.js"),[]),"../data/levels/world-08/level-04.json":()=>h(()=>import("./level-04-CXLEOT8W.js"),[]),"../data/levels/world-08/level-05.json":()=>h(()=>import("./level-05-BkK7yQ74.js"),[]),"../data/levels/world-08/level-06.json":()=>h(()=>import("./level-06-ChSMcjFL.js"),[]),"../data/levels/world-08/level-07.json":()=>h(()=>import("./level-07-DC9t827L.js"),[]),"../data/levels/world-09/level-01.json":()=>h(()=>import("./level-01-Doz7SIZC.js"),[]),"../data/levels/world-09/level-02.json":()=>h(()=>import("./level-02-Cc_6Cmot.js"),[]),"../data/levels/world-09/level-03.json":()=>h(()=>import("./level-03-BARWTjBO.js"),[]),"../data/levels/world-09/level-04.json":()=>h(()=>import("./level-04-GvC9OC2-.js"),[]),"../data/levels/world-09/level-05.json":()=>h(()=>import("./level-05-BLm31U4F.js"),[]),"../data/levels/world-09/level-06.json":()=>h(()=>import("./level-06-BVt2zaY2.js"),[]),"../data/levels/world-09/level-07.json":()=>h(()=>import("./level-07-sS2jK4zz.js"),[]),"../data/levels/world-10/level-01.json":()=>h(()=>import("./level-01-Ds-bJBNu.js"),[]),"../data/levels/world-10/level-02.json":()=>h(()=>import("./level-02-B2M3hLuV.js"),[]),"../data/levels/world-10/level-03.json":()=>h(()=>import("./level-03-COCR62QH.js"),[]),"../data/levels/world-10/level-04.json":()=>h(()=>import("./level-04-DA5Ziyfr.js"),[]),"../data/levels/world-10/level-05.json":()=>h(()=>import("./level-05-z3OB-Eeu.js"),[]),"../data/levels/world-10/level-06.json":()=>h(()=>import("./level-06-BBoPuErM.js"),[]),"../data/levels/world-10/level-07.json":()=>h(()=>import("./level-07-DGoxVMN9.js"),[]),"../data/levels/world-11/level-01.json":()=>h(()=>import("./level-01-Cuww_JdD.js"),[]),"../data/levels/world-11/level-02.json":()=>h(()=>import("./level-02-kZfOdsyi.js"),[]),"../data/levels/world-11/level-03.json":()=>h(()=>import("./level-03-Dh-sXTp4.js"),[]),"../data/levels/world-11/level-04.json":()=>h(()=>import("./level-04-C4pRuHxe.js"),[]),"../data/levels/world-11/level-05.json":()=>h(()=>import("./level-05-BPvlwP0B.js"),[]),"../data/levels/world-11/level-06.json":()=>h(()=>import("./level-06-YLPOBwkX.js"),[]),"../data/levels/world-11/level-07.json":()=>h(()=>import("./level-07-DUX_DajD.js"),[]),"../data/levels/world-12/level-01.json":()=>h(()=>import("./level-01-Dj588ZtM.js"),[]),"../data/levels/world-12/level-02.json":()=>h(()=>import("./level-02-mb3jO4Gw.js"),[]),"../data/levels/world-12/level-03.json":()=>h(()=>import("./level-03-1UlIKD0Q.js"),[]),"../data/levels/world-12/level-04.json":()=>h(()=>import("./level-04-CFRxU1U-.js"),[]),"../data/levels/world-12/level-05.json":()=>h(()=>import("./level-05-B_Hy421Q.js"),[]),"../data/levels/world-12/level-06.json":()=>h(()=>import("./level-06-C_QV9Sef.js"),[]),"../data/levels/world-12/level-07.json":()=>h(()=>import("./level-07-CIN8Pdoi.js"),[]),"../data/levels/world-13/level-01.json":()=>h(()=>import("./level-01-yTjqiIVo.js"),[]),"../data/levels/world-13/level-02.json":()=>h(()=>import("./level-02-BY0K_c1c.js"),[]),"../data/levels/world-13/level-03.json":()=>h(()=>import("./level-03-MtnWjl04.js"),[]),"../data/levels/world-13/level-04.json":()=>h(()=>import("./level-04-ChxeRfoM.js"),[]),"../data/levels/world-13/level-05.json":()=>h(()=>import("./level-05-vX-1EazO.js"),[]),"../data/levels/world-13/level-06.json":()=>h(()=>import("./level-06-pumYmmG_.js"),[]),"../data/levels/world-13/level-07.json":()=>h(()=>import("./level-07-D64cZdPs.js"),[]),"../data/levels/world-14/level-01.json":()=>h(()=>import("./level-01-C6H48JrC.js"),[]),"../data/levels/world-14/level-02.json":()=>h(()=>import("./level-02-CBH7vq4l.js"),[]),"../data/levels/world-14/level-03.json":()=>h(()=>import("./level-03-CWvcUtIp.js"),[]),"../data/levels/world-14/level-04.json":()=>h(()=>import("./level-04-Di0_85nG.js"),[]),"../data/levels/world-14/level-05.json":()=>h(()=>import("./level-05-BKLUPjxr.js"),[]),"../data/levels/world-14/level-06.json":()=>h(()=>import("./level-06-DoXscGRM.js"),[]),"../data/levels/world-14/level-07.json":()=>h(()=>import("./level-07-C9KSb-8I.js"),[]),"../data/levels/world-15/level-01.json":()=>h(()=>import("./level-01-BzlEZR1x.js"),[]),"../data/levels/world-15/level-02.json":()=>h(()=>import("./level-02-DGYWlJPa.js"),[]),"../data/levels/world-15/level-03.json":()=>h(()=>import("./level-03-BaOc1NLt.js"),[]),"../data/levels/world-15/level-04.json":()=>h(()=>import("./level-04-Du7oKhdK.js"),[]),"../data/levels/world-15/level-05.json":()=>h(()=>import("./level-05-BmZzmr9R.js"),[]),"../data/levels/world-15/level-06.json":()=>h(()=>import("./level-06-CsNwuVY9.js"),[]),"../data/levels/world-15/level-07.json":()=>h(()=>import("./level-07-B8UAagNp.js"),[]),"../data/levels/world-16/level-01.json":()=>h(()=>import("./level-01-DgWkLZJS.js"),[]),"../data/levels/world-16/level-02.json":()=>h(()=>import("./level-02-ChtOl_yp.js"),[]),"../data/levels/world-16/level-03.json":()=>h(()=>import("./level-03-h-qfybgt.js"),[]),"../data/levels/world-16/level-04.json":()=>h(()=>import("./level-04-KrfSSPv2.js"),[]),"../data/levels/world-16/level-05.json":()=>h(()=>import("./level-05-1ilFD6pt.js"),[]),"../data/levels/world-16/level-06.json":()=>h(()=>import("./level-06-D49mP320.js"),[]),"../data/levels/world-16/level-07.json":()=>h(()=>import("./level-07-CJ7K6SPY.js"),[]),"../data/levels/world-17/level-01.json":()=>h(()=>import("./level-01-DNjwXgf5.js"),[]),"../data/levels/world-17/level-02.json":()=>h(()=>import("./level-02-Co3Y3obv.js"),[]),"../data/levels/world-17/level-03.json":()=>h(()=>import("./level-03-C8cOJ7jp.js"),[]),"../data/levels/world-17/level-04.json":()=>h(()=>import("./level-04-DXBA7uhw.js"),[]),"../data/levels/world-17/level-05.json":()=>h(()=>import("./level-05-DfAHphhm.js"),[]),"../data/levels/world-17/level-06.json":()=>h(()=>import("./level-06-Bcby5gGS.js"),[]),"../data/levels/world-17/level-07.json":()=>h(()=>import("./level-07-Df086Wg-.js"),[]),"../data/levels/world-18/level-01.json":()=>h(()=>import("./level-01-D35EemEB.js"),[]),"../data/levels/world-18/level-02.json":()=>h(()=>import("./level-02-DHYZ-5nK.js"),[]),"../data/levels/world-18/level-03.json":()=>h(()=>import("./level-03-CNNP6_n1.js"),[]),"../data/levels/world-18/level-04.json":()=>h(()=>import("./level-04-CRWxEHw-.js"),[]),"../data/levels/world-18/level-05.json":()=>h(()=>import("./level-05-CHz8-Ys0.js"),[]),"../data/levels/world-18/level-06.json":()=>h(()=>import("./level-06-6AIqzjqo.js"),[]),"../data/levels/world-18/level-07.json":()=>h(()=>import("./level-07-CrbRIWQ_.js"),[]),"../data/levels/world-19/level-01.json":()=>h(()=>import("./level-01-Blgz7yZo.js"),[]),"../data/levels/world-19/level-02.json":()=>h(()=>import("./level-02-CbK78oZb.js"),[]),"../data/levels/world-19/level-03.json":()=>h(()=>import("./level-03-DKO6TCSI.js"),[]),"../data/levels/world-19/level-04.json":()=>h(()=>import("./level-04-B4iQh-VK.js"),[]),"../data/levels/world-19/level-05.json":()=>h(()=>import("./level-05-upD4TVxi.js"),[]),"../data/levels/world-19/level-06.json":()=>h(()=>import("./level-06-VrjIfOw0.js"),[]),"../data/levels/world-19/level-07.json":()=>h(()=>import("./level-07-Dh_al5rY.js"),[]),"../data/levels/world-20/level-01.json":()=>h(()=>import("./level-01-1lOKw1ZR.js"),[]),"../data/levels/world-20/level-02.json":()=>h(()=>import("./level-02-Dx0ATAOF.js"),[]),"../data/levels/world-20/level-03.json":()=>h(()=>import("./level-03-Dq0YUr1G.js"),[]),"../data/levels/world-20/level-04.json":()=>h(()=>import("./level-04-qLljK_ws.js"),[]),"../data/levels/world-20/level-05.json":()=>h(()=>import("./level-05-DAAq8pkk.js"),[]),"../data/levels/world-20/level-06.json":()=>h(()=>import("./level-06-CSv2DSUp.js"),[]),"../data/levels/world-20/level-07.json":()=>h(()=>import("./level-07-CUDlohcI.js"),[]),"../data/levels/world-21/level-01.json":()=>h(()=>import("./level-01-BpdXI8_S.js"),[]),"../data/levels/world-21/level-02.json":()=>h(()=>import("./level-02-B0da9sR0.js"),[]),"../data/levels/world-21/level-03.json":()=>h(()=>import("./level-03-7NOr76kT.js"),[]),"../data/levels/world-21/level-04.json":()=>h(()=>import("./level-04-DseYTBF4.js"),[]),"../data/levels/world-21/level-05.json":()=>h(()=>import("./level-05-BbGbDCH3.js"),[]),"../data/levels/world-21/level-06.json":()=>h(()=>import("./level-06-B5aq1m-r.js"),[]),"../data/levels/world-21/level-07.json":()=>h(()=>import("./level-07-B_9t2tU4.js"),[]),"../data/levels/world-22/level-01.json":()=>h(()=>import("./level-01-25uGNvf7.js"),[]),"../data/levels/world-22/level-02.json":()=>h(()=>import("./level-02-DxiLbFsB.js"),[]),"../data/levels/world-22/level-03.json":()=>h(()=>import("./level-03-ZZJwDfHi.js"),[]),"../data/levels/world-22/level-04.json":()=>h(()=>import("./level-04-saMFLIo9.js"),[]),"../data/levels/world-22/level-05.json":()=>h(()=>import("./level-05-lojKLp2y.js"),[]),"../data/levels/world-22/level-06.json":()=>h(()=>import("./level-06-93KWENNM.js"),[]),"../data/levels/world-22/level-07.json":()=>h(()=>import("./level-07-B02aV1z0.js"),[]),"../data/levels/world-23/level-01.json":()=>h(()=>import("./level-01-DnicaP6L.js"),[]),"../data/levels/world-23/level-02.json":()=>h(()=>import("./level-02-BhSKlQZB.js"),[]),"../data/levels/world-23/level-03.json":()=>h(()=>import("./level-03-RAWFEPbU.js"),[]),"../data/levels/world-23/level-04.json":()=>h(()=>import("./level-04-B9QGDH3i.js"),[]),"../data/levels/world-23/level-05.json":()=>h(()=>import("./level-05-SOFH2cNi.js"),[]),"../data/levels/world-23/level-06.json":()=>h(()=>import("./level-06-DbfB9Yji.js"),[]),"../data/levels/world-23/level-07.json":()=>h(()=>import("./level-07-B7SMzz5c.js"),[]),"../data/levels/world-24/level-01.json":()=>h(()=>import("./level-01-DgiocTRl.js"),[]),"../data/levels/world-24/level-02.json":()=>h(()=>import("./level-02-Dk_HIC0h.js"),[]),"../data/levels/world-24/level-03.json":()=>h(()=>import("./level-03-Cm33UoKc.js"),[]),"../data/levels/world-24/level-04.json":()=>h(()=>import("./level-04-DdQPSGtz.js"),[]),"../data/levels/world-24/level-05.json":()=>h(()=>import("./level-05-DAzBe-oL.js"),[]),"../data/levels/world-24/level-06.json":()=>h(()=>import("./level-06-D8T7Wfez.js"),[]),"../data/levels/world-24/level-07.json":()=>h(()=>import("./level-07-COxPLkBk.js"),[]),"../data/levels/world-25/level-01.json":()=>h(()=>import("./level-01-GeRc2jVf.js"),[]),"../data/levels/world-25/level-02.json":()=>h(()=>import("./level-02-jc4T91zx.js"),[]),"../data/levels/world-25/level-03.json":()=>h(()=>import("./level-03-CZe_lqht.js"),[]),"../data/levels/world-25/level-04.json":()=>h(()=>import("./level-04-aiCPwTSZ.js"),[]),"../data/levels/world-25/level-05.json":()=>h(()=>import("./level-05-F-HvBjdP.js"),[]),"../data/levels/world-25/level-06.json":()=>h(()=>import("./level-06-G51qjiW9.js"),[]),"../data/levels/world-25/level-07.json":()=>h(()=>import("./level-07-CcQr2J3j.js"),[]),"../data/levels/world-26/level-01.json":()=>h(()=>import("./level-01-2U-kKtcy.js"),[]),"../data/levels/world-26/level-02.json":()=>h(()=>import("./level-02-CA3JCsMC.js"),[]),"../data/levels/world-26/level-03.json":()=>h(()=>import("./level-03-1gqmi-c2.js"),[]),"../data/levels/world-26/level-04.json":()=>h(()=>import("./level-04-BS54ZD5l.js"),[]),"../data/levels/world-26/level-05.json":()=>h(()=>import("./level-05-CYxA6iTA.js"),[]),"../data/levels/world-26/level-06.json":()=>h(()=>import("./level-06-DUDtfRgh.js"),[]),"../data/levels/world-26/level-07.json":()=>h(()=>import("./level-07-BLoL4zrN.js"),[]),"../data/levels/world-27/level-01.json":()=>h(()=>import("./level-01-D7hzpPMW.js"),[]),"../data/levels/world-27/level-02.json":()=>h(()=>import("./level-02-BZs-ATyq.js"),[]),"../data/levels/world-27/level-03.json":()=>h(()=>import("./level-03-D2pXEkxA.js"),[]),"../data/levels/world-27/level-04.json":()=>h(()=>import("./level-04-CAX1ABug.js"),[]),"../data/levels/world-27/level-05.json":()=>h(()=>import("./level-05-DPAz1rqa.js"),[]),"../data/levels/world-27/level-06.json":()=>h(()=>import("./level-06-DZJ7vlb_.js"),[]),"../data/levels/world-27/level-07.json":()=>h(()=>import("./level-07-BXf_yGmd.js"),[]),"../data/levels/world-28/level-01.json":()=>h(()=>import("./level-01-NEcNW_tg.js"),[]),"../data/levels/world-28/level-02.json":()=>h(()=>import("./level-02-CX06_L5l.js"),[]),"../data/levels/world-28/level-03.json":()=>h(()=>import("./level-03-BZy_FmET.js"),[]),"../data/levels/world-28/level-04.json":()=>h(()=>import("./level-04-BX037YHv.js"),[]),"../data/levels/world-28/level-05.json":()=>h(()=>import("./level-05-nIhHYWqb.js"),[]),"../data/levels/world-28/level-06.json":()=>h(()=>import("./level-06-CeZbb6CD.js"),[]),"../data/levels/world-28/level-07.json":()=>h(()=>import("./level-07-m9ebWp0x.js"),[]),"../data/levels/world-29/level-01.json":()=>h(()=>import("./level-01-DlXETsa4.js"),[]),"../data/levels/world-29/level-02.json":()=>h(()=>import("./level-02-B0A9jMbv.js"),[]),"../data/levels/world-29/level-03.json":()=>h(()=>import("./level-03-CKTmgbQy.js"),[]),"../data/levels/world-29/level-04.json":()=>h(()=>import("./level-04-Ep5jyKM3.js"),[]),"../data/levels/world-29/level-05.json":()=>h(()=>import("./level-05-qEr3zVtb.js"),[]),"../data/levels/world-29/level-06.json":()=>h(()=>import("./level-06-DmZuma_T.js"),[]),"../data/levels/world-29/level-07.json":()=>h(()=>import("./level-07-DP66zCb0.js"),[]),"../data/levels/world-30/level-01.json":()=>h(()=>import("./level-01-BnXa06pH.js"),[]),"../data/levels/world-30/level-02.json":()=>h(()=>import("./level-02-CfUtjzJY.js"),[]),"../data/levels/world-30/level-03.json":()=>h(()=>import("./level-03-CRfjVDON.js"),[]),"../data/levels/world-30/level-04.json":()=>h(()=>import("./level-04-d7E5I5zj.js"),[]),"../data/levels/world-30/level-05.json":()=>h(()=>import("./level-05-DCC6jkWF.js"),[]),"../data/levels/world-30/level-06.json":()=>h(()=>import("./level-06-DG68dGRh.js"),[]),"../data/levels/world-30/level-07.json":()=>h(()=>import("./level-07-BQYdF1vi.js"),[])}),k=class k{static async loadLevel(t){if(k.cache.has(t))return k.cache.get(t);const e=k.levelIdToPath(t);if(!e)return console.warn(`[LevelManager] Cannot resolve path for levelId: ${t}`),null;try{const s=`../data/levels/${k.levelIdToRelative(t)}`,i=Yt[s];if(!i)return console.warn(`[LevelManager] No bundled module for: ${s}`),null;const o=await i(),a=o.default??o;return k.cache.set(t,a),a}catch(s){return console.error(`[LevelManager] Failed to load level "${t}" from "${e}":`,s),null}}static levelIdToPath(t){const e=k.levelIdToRelative(t);return e?`src/data/levels/${e}`:""}static levelIdToRelative(t){const e=t.match(/^(world-\d{2})-level-(\d{2})$/);if(e)return`${e[1]}/level-${e[2]}.json`;const s=t.match(/^(world-\d{2})-boss$/);return s?`${s[1]}/level-06.json`:""}static getLevelIdsForWorld(t){if(t===7)return[];const e=`world-${String(t).padStart(2,"0")}`,s=[];for(let i=1;i<=6;i++)s.push(`${e}-level-${String(i).padStart(2,"0")}`);return s}static getLevelProgress(t){try{const e=localStorage.getItem(`level_progress_${t}`);return e?JSON.parse(e):null}catch{return null}}static saveLevelProgress(t,e){try{const s=k.getLevelProgress(t),i={levelId:t,stars:Math.max(e.stars,(s==null?void 0:s.stars)??0),bestLives:Math.max(e.bestLives,(s==null?void 0:s.bestLives)??0),bestGold:Math.max(e.bestGold,(s==null?void 0:s.bestGold)??0),completed:e.completed||((s==null?void 0:s.completed)??!1)};localStorage.setItem(`level_progress_${t}`,JSON.stringify(i))}catch(s){console.error("[LevelManager] Could not save level progress:",s)}}};r(k,"cache",new Map);let ke=k;const Xt={"freeze-all":(d,t=2500)=>{l.emit("ability:freeze-all",{duration:t})},"gold-multiplier":(d=3,t=12e3)=>{l.emit("ability:gold-multiplier",{value:d,duration:t})},"tower-speed-multiplier":(d=2,t=6e3)=>{l.emit("ability:tower-surge",{value:d,duration:t})},"aoe-damage":(d=300)=>{l.emit("ability:aoe-damage",{damage:d})},"restore-lives":(d=3)=>{l.emit(p.LIVES_CHANGED,d)},"stun-bosses":(d,t=4e3)=>{l.emit("ability:stun-bosses",{duration:t})}};class qt{constructor(t,e){r(this,"scene");r(this,"worldId");r(this,"abilities",[]);r(this,"waveCount",0);r(this,"onWaveCompleted",()=>{this.waveCount++;let t=!1;this.abilities.forEach(e=>{e.earnedEveryNWaves&&this.waveCount%e.earnedEveryNWaves===0&&(e.charges++,_.abilityCharges[e.id]=e.charges,t=!0)}),t&&l.emit("ability:charges-changed",this.getChargesSnapshot())});r(this,"onBossKilled",()=>{let t=!1;this.abilities.forEach(e=>{e.earnedOn==="boss-kill"&&(e.charges++,_.abilityCharges[e.id]=e.charges,t=!0)}),t&&l.emit("ability:charges-changed",this.getChargesSnapshot())});r(this,"onUseAbility",t=>{this.useAbility(t)});this.scene=t,this.worldId=e,!(e<21)&&(this.loadAbilitiesForWorld(),this.registerListeners())}getAbilities(){return this.abilities}useAbility(t){const e=this.abilities.find(s=>s.id===t);return!e||e.charges<=0?!1:(e.charges--,_.abilityCharges[t]=e.charges,h(()=>import("./items-BixPSnJ-.js"),[]).then(s=>{if(!this.scene.sys.isActive())return;const o=(s.default??s).find(a=>a.id===t);if(o){const a=Xt[o.effectType];a&&a(o.value,o.duration)}}).catch(()=>{}),l.emit("ability:charges-changed",this.getChargesSnapshot()),!0)}destroy(){l.off(p.WAVE_COMPLETED,this.onWaveCompleted,this),l.off(p.BOSS_DIED,this.onBossKilled,this),this.scene.events.off("ui:use-ability",this.onUseAbility,this)}loadAbilitiesForWorld(){const t=[{id:"ability-freeze-pulse",name:"Freeze Pulse",icon:"❄️",charges:0,earnedEveryNWaves:2},{id:"ability-gold-rush",name:"Gold Rush",icon:"💰",charges:0,earnedEveryNWaves:3},{id:"ability-tower-surge",name:"Tower Surge",icon:"⚡",charges:0,earnedEveryNWaves:3},{id:"ability-landslide",name:"Landslide",icon:"🪨",charges:0,earnedEveryNWaves:2},{id:"ability-healing-spring",name:"Healing Spring",icon:"💧",charges:0,earnedEveryNWaves:4},{id:"ability-boss-stun",name:"Boss Stun",icon:"💥",charges:0,earnedOn:"boss-kill"}],s={21:["ability-landslide","ability-freeze-pulse"],22:["ability-freeze-pulse","ability-tower-surge"],23:["ability-boss-stun","ability-gold-rush"],24:["ability-freeze-pulse","ability-landslide"],25:["ability-tower-surge","ability-healing-spring"],26:["ability-landslide","ability-gold-rush"],27:["ability-boss-stun","ability-freeze-pulse"],28:["ability-tower-surge","ability-landslide"],29:["ability-boss-stun","ability-healing-spring"],30:["ability-freeze-pulse","ability-gold-rush","ability-tower-surge","ability-landslide","ability-healing-spring","ability-boss-stun"]}[this.worldId]??[];this.abilities=t.filter(i=>s.includes(i.id)),this.abilities.forEach(i=>{_.abilityCharges[i.id]!==void 0&&(i.charges=_.abilityCharges[i.id])})}registerListeners(){l.on(p.WAVE_COMPLETED,this.onWaveCompleted,this),l.on(p.BOSS_DIED,this.onBossKilled,this),this.scene.events.on("ui:use-ability",this.onUseAbility,this)}getChargesSnapshot(){return Object.fromEntries(this.abilities.map(t=>[t.id,t.charges]))}}const Qt=2e3,Jt=100,Zt=500,es=1.25,ts=[{minCombo:50,multiplier:3},{minCombo:25,multiplier:2.5},{minCombo:10,multiplier:2},{minCombo:5,multiplier:1.5},{minCombo:1,multiplier:1}],ss=new Set([10,25,50]);class is{constructor(t){r(this,"scene");r(this,"state");r(this,"decayTimer");this.scene=t,this.state={count:0,multiplier:1,lastKillTime:0,expTotal:0,expForNextCharge:Zt},l.on(p.ENEMY_DIED,this.onEnemyKilled,this),this.decayTimer=this.scene.time.addEvent({delay:Jt,callback:this.tickDecay,callbackScope:this,loop:!0})}getState(){return{...this.state}}destroy(){l.off(p.ENEMY_DIED,this.onEnemyKilled,this),this.decayTimer&&this.decayTimer.remove(!1)}onEnemyKilled(t){const{x:e,y:s,reward:i,hp:o}=t,a=this.scene.time.now;this.state.count+=1,this.state.lastKillTime=a,this.state.multiplier=this.resolveMultiplier(this.state.count),l.emit("combo:updated",{count:this.state.count,multiplier:this.state.multiplier,x:e,y:s}),ss.has(this.state.count)&&l.emit("combo:fanfare",{milestone:this.state.count,x:e,y:s});const n=Math.floor(i*(this.state.multiplier-1));n>0&&l.emit("combo:gold-bonus",{bonus:n,x:e,y:s});const c=o>0&&i>0?o/i:1,m=i*.1*Math.min(c,2);this.awardExp(m)}awardExp(t){for(this.state.expTotal+=t;this.state.expTotal>=this.state.expForNextCharge;)this.state.expTotal-=this.state.expForNextCharge,this.state.expForNextCharge=Math.ceil(this.state.expForNextCharge*es),l.emit("exp:charge-earned",{});l.emit("exp:updated",{current:this.state.expTotal,max:this.state.expForNextCharge,percent:this.state.expTotal/this.state.expForNextCharge})}tickDecay(){if(this.state.count===0)return;this.scene.time.now>this.state.lastKillTime+Qt&&this.resetCombo()}resetCombo(){this.state.count=0,this.state.multiplier=1,l.emit("combo:reset",{})}resolveMultiplier(t){for(const e of ts)if(t>=e.minCombo)return e.multiplier;return 1}}const le=14,os=20,as=4,rs=1200,et=8e3,ns="Back.easeOut",ls=1.6,ds={"gold-coin":{color:16768256,emoji:"💰",label:"GOLD"},"speed-boost":{color:56831,emoji:"⚡",label:"SPEED"},shield:{color:4491519,emoji:"🛡️",label:"SHIELD"},bomb:{color:16729088,emoji:"💣",label:"BOMB"},heal:{color:4521796,emoji:"❤️",label:"HEAL"}},Ce=[{type:"gold-coin",chance:.25},{type:"speed-boost",chance:.08},{type:"shield",chance:.05},{type:"bomb",chance:.06},{type:"heal",chance:.04}];function hs(){const d=Math.random();let t=0;for(const e of Ce)if(t+=e.chance,d{const s=hs();s!==null&&this.spawnDrop(e.x??0,e.y??0,s)},this.onBossDied=e=>{const s=["heal","gold-coin",cs()],i=e.x??0,o=e.y??0;s.forEach((a,n)=>{this.scene.time.delayedCall(n*120,()=>{const c=ps(i,o,n,s.length);this.spawnDrop(c.x,c.y,a)})})},l.on(p.ENEMY_DIED,this.onEnemyKilled,this),l.on(p.BOSS_DIED,this.onBossDied,this)}spawnDrop(t,e,s){const i=ds[s],o=this.scene.add.circle(t,e,os,i.color,.25);o.setDepth(10);const a=this.scene.add.circle(t,e,le,i.color,1);a.setDepth(11),a.setStrokeStyle(2,16777215,.6);const n=this.scene.add.text(t,e-1,i.emoji,{fontSize:"14px",align:"center"});n.setOrigin(.5,.5),n.setDepth(12),a.setInteractive(new u.Geom.Circle(le,le,le+4),u.Geom.Circle.Contains),a.on(u.Input.Events.POINTER_OVER,()=>{g.collected||(this.scene.tweens.add({targets:[o],alpha:.55,scale:1.25,duration:120,ease:"Sine.easeOut"}),this.scene.input.setDefaultCursor("pointer"))}),a.on(u.Input.Events.POINTER_OUT,()=>{g.collected||(this.scene.tweens.add({targets:[o],alpha:.25,scale:1,duration:120,ease:"Sine.easeOut"}),this.scene.input.setDefaultCursor("default"))}),a.on(u.Input.Events.POINTER_DOWN,()=>{g.collected||this.collectDrop(g)}),a.setScale(0),o.setScale(0),n.setScale(0),this.scene.tweens.add({targets:[a,o,n],scale:1,duration:380,ease:ns}),this.scene.time.delayedCall(380,()=>{this.scene.tweens.add({targets:o,scale:1.4,alpha:0,duration:300,ease:"Sine.easeOut",onComplete:()=>{o.setAlpha(.25),o.setScale(1)}})});const c=this.scene.tweens.add({targets:[a,o,n],y:`+=${as}`,duration:rs,ease:"Sine.easeInOut",yoyo:!0,repeat:-1}),m=et-2e3,f=this.scene.tweens.add({targets:[a,o,n],alpha:0,duration:600,ease:"Sine.easeIn",delay:et-600,onStart:()=>{this.scene.tweens.add({targets:[a,o,n],alpha:.2,duration:160,ease:"Linear",yoyo:!0,repeat:5,delay:m})},onComplete:()=>{this.destroyDrop(g)}}),g={circle:a,label:n,glow:o,floatTween:c,expireTween:f,type:s,collected:!1};this.activeDrops.push(g)}destroy(){l.off(p.ENEMY_DIED,this.onEnemyKilled,this),l.off(p.BOSS_DIED,this.onBossDied,this),[...this.activeDrops].forEach(t=>this.destroyDrop(t)),this.activeDrops=[]}collectDrop(t){t.collected=!0,t.floatTween.stop(),t.expireTween.stop(),this.scene.input.setDefaultCursor("default"),this.applyEffect(t.type),this.scene.tweens.add({targets:[t.circle,t.glow,t.label],scale:ls,alpha:0,duration:260,ease:"Cubic.easeOut",onComplete:()=>{this.destroyDrop(t)}}),l.emit("drop:collected",{type:t.type})}applyEffect(t){switch(t){case"gold-coin":{const e=u.Math.Between(15,50);l.emit("drop:gold",{amount:e});break}case"speed-boost":l.emit("drop:speed-boost",{duration:8e3});break;case"shield":l.emit("drop:shield",{});break;case"bomb":l.emit("drop:bomb",{});break;case"heal":l.emit("drop:heal",{amount:2});break}}destroyDrop(t){var s,i,o,a,n;(s=t.floatTween)!=null&&s.isPlaying()&&t.floatTween.stop(),(i=t.expireTween)!=null&&i.isPlaying()&&t.expireTween.stop(),(o=t.circle)!=null&&o.active&&(t.circle.removeAllListeners(),t.circle.destroy()),(a=t.label)!=null&&a.active&&t.label.destroy(),(n=t.glow)!=null&&n.active&&t.glow.destroy();const e=this.activeDrops.indexOf(t);e!==-1&&this.activeDrops.splice(e,1)}}const us={damage:{color:"#ffffff",fontSize:18,bold:!1},crit:{color:"#ff4444",fontSize:26,bold:!0},exp:{color:"#44ffff",fontSize:16,bold:!1},gold:{color:"#ffdd00",fontSize:16,bold:!1},combo:{color:"#ff8800",fontSize:22,bold:!0},fanfare:{color:"#dd44ff",fontSize:36,bold:!0,stroke:"#ffffff",strokeThickness:4},powerup:{color:"#44ff44",fontSize:18,bold:!0},heal:{color:"#88ff88",fontSize:18,bold:!1}},fs={damage:1100,crit:1100,exp:1100,gold:1100,combo:800,fanfare:1800,powerup:1100,heal:1100},gs={damage:70,crit:80,exp:70,gold:70,combo:60,fanfare:0,powerup:70,heal:70},_s=20;class ys{constructor(t){r(this,"scene");r(this,"pool",[]);this.scene=t,this._preallocate(_s)}show(t,e,s,i){const o=this._acquire(),a=us[i],n=fs[i],c=gs[i];o.setText(s),o.setStyle({color:a.color,fontSize:`${a.fontSize}px`,fontStyle:a.bold?"bold":"normal",fontFamily:"Arial, sans-serif",stroke:a.stroke??"",strokeThickness:a.strokeThickness??0}),o.setDepth(1e3),i==="fanfare"?this._animateFanfare(o,n):i==="crit"?this._animateCrit(o,t,e,c,n):i==="combo"?this._animateCombo(o,t,e,c,n):this._animateDefault(o,t,e,c,n)}destroy(){for(const t of this.pool)this.scene.tweens.killTweensOf(t),t.destroy();this.pool=[]}_preallocate(t){for(let e=0;ethis._release(t,a,n)})}_animateCrit(t,e,s,i,o){const a=e,n=s,c=12;t.setPosition(a,n),t.setScale(1.3),t.setAlpha(1),this.scene.tweens.add({targets:t,y:n+c,scaleX:1,scaleY:1,duration:80,ease:"Quad.easeIn",onComplete:()=>{this.scene.tweens.add({targets:t,y:n-i,alpha:0,duration:o-80,ease:"Quad.easeOut",onComplete:()=>this._release(t,a,n)})}})}_animateCombo(t,e,s,i,o){const a=e,n=s-30,c=.4;t.setPosition(a,n),t.setScale(c),t.setAlpha(1),this.scene.tweens.add({targets:t,scaleX:1.2,scaleY:1.2,duration:120,ease:"Back.easeOut",onComplete:()=>{this.scene.tweens.add({targets:t,scaleX:1,scaleY:1,duration:80,ease:"Quad.easeOut",onComplete:()=>{this.scene.tweens.add({targets:t,y:n-i,alpha:0,duration:o-200,ease:"Quad.easeOut",onComplete:()=>this._release(t,a,n)})}})}})}_animateFanfare(t,e){const s=this.scene.cameras.main,i=s.scrollX+s.width/2,o=s.scrollY+s.height/2,a=i,n=o;t.setPosition(a,n),t.setScale(.5),t.setAlpha(0);const c=e-400,m=400;this.scene.tweens.add({targets:t,scaleX:1,scaleY:1,alpha:1,duration:300,ease:"Back.easeOut",onComplete:()=>{this.scene.tweens.add({targets:t,scaleX:1.05,scaleY:1.05,duration:Math.min(c/2,200),ease:"Sine.easeInOut",yoyo:!0,repeat:Math.floor(c/400),onComplete:()=>{this.scene.tweens.add({targets:t,alpha:0,scaleX:1.3,scaleY:1.3,duration:m,ease:"Quad.easeIn",onComplete:()=>this._release(t,a,n)})}})}})}}const ws=[{id:"first_blood",label:"🩸 FIRST BLOOD!",once:!0},{id:"boss_slayer",label:"💀 BOSS SLAYER!",once:!1},{id:"wave_5",label:"🌊 WAVE 5 CLEARED!",once:!0},{id:"wave_10",label:"🔟 WAVE 10 CLEARED!",once:!0},{id:"wave_master",label:"🏆 WAVE MASTER!",once:!0},{id:"gold_hoard",label:"💎 GOLD HOARD!",once:!0}];class vs{constructor(t,e){r(this,"scene");r(this,"floatingText");r(this,"earned",new Set);r(this,"killCount",0);r(this,"waveReached",0);r(this,"onEnemyKilled");r(this,"onBossDied");r(this,"onWaveCompleted");r(this,"onGoldChanged");this.scene=t,this.floatingText=e,this.onEnemyKilled=()=>{this.killCount+=1,this.killCount===1&&this.unlock("first_blood")},this.onBossDied=()=>{this.unlock("boss_slayer")},this.onWaveCompleted=s=>{this.waveReached+=1,this.waveReached===5&&this.unlock("wave_5"),this.waveReached===10&&this.unlock("wave_10")},this.onGoldChanged=s=>{s>=5e3&&this.unlock("gold_hoard")},l.on(p.ENEMY_DIED,this.onEnemyKilled,this),l.on(p.BOSS_DIED,this.onBossDied,this),l.on(p.WAVE_COMPLETED,this.onWaveCompleted,this),l.on(p.GOLD_CHANGED,this.onGoldChanged,this)}unlock(t){const e=ws.find(s=>s.id===t);e&&(e.once&&this.earned.has(t)||(e.once&&this.earned.add(t),this.showToast(e.label)))}destroy(){l.off(p.ENEMY_DIED,this.onEnemyKilled,this),l.off(p.BOSS_DIED,this.onBossDied,this),l.off(p.WAVE_COMPLETED,this.onWaveCompleted,this),l.off(p.GOLD_CHANGED,this.onGoldChanged,this)}showToast(t){const e=this.scene.cameras.main,s=e.scrollX+e.width*.5,i=e.scrollY+e.height*.72;this.floatingText.show(s,i,`🏅 ACHIEVEMENT +${t}`,"powerup")}}let K=null;function fe(){return(!K||K.state==="closed")&&(K=new AudioContext),K.state==="suspended"&&K.resume(),K}function v(d,t,e,s,i,o,a=0,n){const c=d.currentTime+a/1e3,m=c+o/1e3,f=d.createGain();f.gain.setValueAtTime(0,c),f.gain.linearRampToValueAtTime(i,c+.004),f.gain.setTargetAtTime(0,m-.025,.012),f.connect(t);const g=d.createOscillator();g.type=e,g.frequency.setValueAtTime(s,c),n!==void 0&&g.frequency.linearRampToValueAtTime(n,m),g.connect(f),g.start(c),g.stop(m+.06)}function L(d,t,e,s,i,o,a=0){const n=d.currentTime+a/1e3,c=Math.ceil(d.sampleRate*(o/1e3)),m=d.createBuffer(1,c,d.sampleRate),f=m.getChannelData(0);for(let E=0;E{v(d,t,"square",380,e*.18,55)},crit:(d,t,e)=>{v(d,t,"square",780,e*.38,90,0,280),L(d,t,3500,.8,e*.22,60)},enemy_die:(d,t,e)=>{v(d,t,"sawtooth",280,e*.22,160,0,70)},boss_die:(d,t,e)=>{v(d,t,"sine",55,e*.55,700,0,28),v(d,t,"sawtooth",140,e*.3,450,40,38),L(d,t,2200,.5,e*.28,220,0),L(d,t,600,1.2,e*.35,500,60)},wave_start:(d,t,e)=>{v(d,t,"sine",75,e*.28,520,0,130),v(d,t,"sine",150,e*.18,320,180)},wave_complete:(d,t,e)=>{[523,659,784].forEach((i,o)=>v(d,t,"sine",i,e*.28,220,o*110))},victory:(d,t,e)=>{[523,587,659,698,784,880,988,1047].forEach((i,o)=>v(d,t,"sine",i,e*.3,280,o*90)),[523,659,784].forEach(i=>v(d,t,"sine",i/2,e*.15,900,200))},defeat:(d,t,e)=>{[440,415,392,370,349,330,311,293].forEach((i,o)=>v(d,t,"sine",i,e*.25,340,o*130)),v(d,t,"sawtooth",110,e*.2,1200,0,55)},tower_place:(d,t,e)=>{v(d,t,"sine",200,e*.32,130,0,90),L(d,t,800,1.5,e*.12,80)},tower_upgrade:(d,t,e)=>{v(d,t,"sine",659,e*.28,160,0),v(d,t,"sine",880,e*.34,200,150),v(d,t,"sine",1318,e*.22,180,310)},tower_sell:(d,t,e)=>{v(d,t,"sine",440,e*.24,220,0,300)}};function Fe(d,t=.28){var e;try{const s=fe(),i=s.createGain();i.gain.value=t,i.connect(s.destination),(e=ye[d])==null||e.call(ye,s,i,1),setTimeout(()=>{try{i.disconnect()}catch{}},3e3)}catch{}}const Es={hit:80,crit:120,enemy_die:100};class Ss{constructor(t){r(this,"muted",!1);r(this,"masterVol",.28);r(this,"lastPlayed",new Map);r(this,"_onDamage");r(this,"_onEnemyDie");r(this,"_onBossDie");r(this,"_onWaveStart");r(this,"_onWaveEnd");r(this,"_onPlace");r(this,"_onUpgrade");r(this,"_onSell");r(this,"_onSetVolume");fe(),this.masterVol=_.settings.sfxVolume,this._onDamage=e=>this._play(e.isCrit?"crit":"hit"),this._onEnemyDie=()=>this._play("enemy_die"),this._onBossDie=()=>this._play("boss_die"),this._onWaveStart=()=>this._play("wave_start"),this._onWaveEnd=()=>this._play("wave_complete"),this._onPlace=()=>this._play("tower_place"),this._onUpgrade=()=>this._play("tower_upgrade"),this._onSell=()=>this._play("tower_sell"),this._onSetVolume=e=>this.setVolume(e.volume),l.on(p.DAMAGE_DEALT,this._onDamage,this),l.on(p.ENEMY_DIED,this._onEnemyDie,this),l.on(p.BOSS_DIED,this._onBossDie,this),l.on(p.WAVE_STARTED,this._onWaveStart,this),l.on(p.WAVE_COMPLETED,this._onWaveEnd,this),l.on(p.TOWER_PLACED,this._onPlace,this),l.on(p.TOWER_UPGRADED,this._onUpgrade,this),l.on(p.TOWER_SOLD,this._onSell,this),l.on(p.SET_SFX_VOLUME,this._onSetVolume,this)}setMuted(t){this.muted=t}getMuted(){return this.muted}setVolume(t){this.masterVol=Math.max(0,Math.min(1,t))}getVolume(){return this.masterVol}destroy(){l.off(p.DAMAGE_DEALT,this._onDamage,this),l.off(p.ENEMY_DIED,this._onEnemyDie,this),l.off(p.BOSS_DIED,this._onBossDie,this),l.off(p.WAVE_STARTED,this._onWaveStart,this),l.off(p.WAVE_COMPLETED,this._onWaveEnd,this),l.off(p.TOWER_PLACED,this._onPlace,this),l.off(p.TOWER_UPGRADED,this._onUpgrade,this),l.off(p.TOWER_SOLD,this._onSell,this),l.off(p.SET_SFX_VOLUME,this._onSetVolume,this)}_play(t){if(this.muted)return;const e=Es[t];if(e!==void 0){const s=Date.now(),i=this.lastPlayed.get(t)??0;if(s-i{v(d,t,"sine",2400,e*.08,25)},ui_click:(d,t,e)=>{v(d,t,"square",1200,e*.14,35)},ui_menu_open:(d,t,e)=>{v(d,t,"sine",880,e*.18,100,0),v(d,t,"sine",1320,e*.16,120,80)},ui_menu_close:(d,t,e)=>{v(d,t,"sine",1320,e*.16,100,0),v(d,t,"sine",880,e*.14,120,80)}};class Ts{constructor(){r(this,"volume");r(this,"_onSetVolume");this.volume=_.settings.sfxVolume,this._onSetVolume=t=>{this.volume=Math.max(0,Math.min(1,t.volume))},l.on(p.SET_SFX_VOLUME,this._onSetVolume,this)}play(t){try{const e=fe(),s=e.createGain();s.gain.value=this.volume,s.connect(e.destination),bs[t](e,s,1),setTimeout(()=>{try{s.disconnect()}catch{}},1e3)}catch{}}setVolume(t){this.volume=Math.max(0,Math.min(1,t))}destroy(){l.off(p.SET_SFX_VOLUME,this._onSetVolume,this)}}const As={fire:(d,t,e)=>{L(d,t,800,1.2,e*.22,120),v(d,t,"sine",180,e*.12,100)},ice:(d,t,e)=>{L(d,t,4e3,2,e*.18,40),v(d,t,"sine",3200,e*.06,20)},lightning:(d,t,e)=>{v(d,t,"square",800,e*.2,60,0,200)},stone:(d,t,e)=>{v(d,t,"sine",120,e*.28,100),L(d,t,300,1.5,e*.15,150)},poison:(d,t,e)=>{v(d,t,"sine",300,e*.12,30,0),v(d,t,"sine",450,e*.12,30,35),v(d,t,"sine",600,e*.12,30,70)},magic:(d,t,e)=>{v(d,t,"triangle",1e3,e*.14,80,0,800),L(d,t,2e3,.8,e*.06,60)},physical:(d,t,e)=>{v(d,t,"sine",160,e*.06,50)}},Ds={burn:(d,t,e)=>{L(d,t,900,1,e*.1,60)},freeze:(d,t,e)=>{v(d,t,"sine",3600,e*.08,25),L(d,t,5e3,2,e*.06,30)},slow:(d,t,e)=>{v(d,t,"triangle",600,e*.08,60,0,400)},stun:(d,t,e)=>{v(d,t,"square",1400,e*.1,30)},poison:(d,t,e)=>{v(d,t,"sine",350,e*.08,25,0),v(d,t,"sine",500,e*.08,25,30)}};function xs(d,t,e){v(d,t,"sawtooth",220,e*.2,300,0),v(d,t,"sine",277,e*.18,280,20),v(d,t,"sine",330,e*.16,260,40),v(d,t,"triangle",415,e*.12,240,60),L(d,t,1800,.6,e*.24,180,0),L(d,t,600,1.5,e*.16,250,50)}const we=125;class Ps{constructor(){r(this,"volume");r(this,"lastImpact",0);r(this,"lastStatus",0);r(this,"lastSynergy",0);r(this,"_onImpact");r(this,"_onStatus");r(this,"_onSynergy");r(this,"_onSetVolume");this.volume=_.settings.sfxVolume,this._onImpact=t=>this.playImpactSound(t.element),this._onStatus=t=>this.playStatusSound(t.type),this._onSynergy=()=>this.playSynergySound(),this._onSetVolume=t=>{this.volume=Math.max(0,Math.min(1,t.volume))},l.on("vfx:impact-played",this._onImpact,this),l.on("vfx:status-applied",this._onStatus,this),l.on("vfx:synergy-played",this._onSynergy,this),l.on(p.SET_SFX_VOLUME,this._onSetVolume,this)}playImpactSound(t){if(t==="physical")return;const e=Date.now();e-this.lastImpact{try{s.disconnect()}catch{}},2e3)}catch{}}destroy(){l.off("vfx:impact-played",this._onImpact,this),l.off("vfx:status-applied",this._onStatus,this),l.off("vfx:synergy-played",this._onSynergy,this),l.off(p.SET_SFX_VOLUME,this._onSetVolume,this)}}function $(d){return Math.min(1,Math.log10(Math.max(1,d))/3)}let rt=1;function Ls(d){rt=Math.max(.1,Math.min(1,d))}function Rs(d,t,e){const s=1+$(d)*4,i=.5+Math.min(3.5,t/40),a={physical:1,fire:1.3,ice:1,lightning:.8,magic:1.1,poison:1.2,stone:1.1}[e];return Math.round(30*s*i*a*rt)}class Os{constructor(t){r(this,"scene");r(this,"camera");this.scene=t,this.camera=t.cameras.main}shake(t){const e=$(t);if(e<.4)return;const s=.002+e*.013,i=50+Math.round(e*250);this.camera.shake(i,s)}flash(t=16777215,e=.3,s=120){const i=t>>16&255,o=t>>8&255,a=t&255;this.camera.flash(s,i,o,a),this.camera.flashEffect.alpha=e}slowMotion(t=.3,e=1e3){this.scene.time.timeScale=t,this.scene.time.delayedCall(e*t,()=>{this.scene.time.timeScale=1})}zoomPulse(t=1.05,e=300){this.scene.tweens.add({targets:this.camera,zoom:t,duration:e/2,ease:"Quad.easeOut",yoyo:!0})}destroy(){}}const Is={physical:{fill:2236962,stroke:3355443,alpha:.5},fire:{fill:4465152,stroke:16737792,alpha:.6},ice:{fill:8965375,stroke:11197951,alpha:.4},lightning:{fill:3351040,stroke:16777028,alpha:.3},magic:{fill:4456550,stroke:10044671,alpha:.5},poison:{fill:2245632,stroke:4521728,alpha:.5},stone:{fill:3355443,stroke:5592405,alpha:.6}},ks=30;class Cs{constructor(t,e){r(this,"scene");r(this,"layer");r(this,"active",[]);this.scene=t,this.layer=e}isOnScreen(t,e,s=100){const i=this.scene.cameras.main;return t>=i.scrollX-s&&t<=i.scrollX+i.width+s&&e>=i.scrollY-s&&e<=i.scrollY+i.height+s}spawn(t,e,s,i){if(!this.isOnScreen(t,e))return;const o=$(s),a=8+Math.round(o*92),n=3e3+Math.round(o*4e3);this.active.length>=ks&&this.active.shift().gfx.destroy();const c=Is[i],m=this.scene.add.graphics();if(m.setDepth(1),m.fillStyle(c.fill,c.alpha),m.fillCircle(t,e,a),m.lineStyle(1.5,c.stroke,c.alpha*.8),m.strokeCircle(t,e,a),i==="physical"||i==="stone"){const f=3+Math.round(o*5);for(let g=0;g=0;e--){const s=this.active[e];s.ttl-=t;const i=s.ttl/s.maxTtl;i<.4&&s.gfx.setAlpha(i/.4),s.ttl<=0&&(s.gfx.destroy(),this.active.splice(e,1))}}destroy(){for(const t of this.active)t.gfx.destroy();this.active.length=0}}const tt={physical:"vfx-damage",fire:"vfx-fire",ice:"vfx-freeze",lightning:"vfx-lightning",magic:"vfx-damage",poison:"vfx-poison",stone:"vfx-stone"},Ms={"vfx-damage":10,"vfx-fire":19,"vfx-freeze":16,"vfx-lightning":14,"vfx-poison":40,"vfx-stone":18,"vfx-time":14,"vfx-defense":10},Bs=50;class Vs{constructor(t,e){r(this,"scene");r(this,"layer");r(this,"pool",[]);r(this,"animsRegistered",!1);this.scene=t,this.layer=e,this.registerAnimations(),this.initPool()}isOnScreen(t,e,s=100){const i=this.scene.cameras.main;return t>=i.scrollX-s&&t<=i.scrollX+i.width+s&&e>=i.scrollY-s&&e<=i.scrollY+i.height+s}playImpact(t,e,s,i){if(!this.isOnScreen(t,e))return;const o=this.acquire();if(!o)return;const n=.3+$(s)*2.2,c=tt[i];o.setPosition(t,e),o.setScale(n),o.setAlpha(1),o.setDepth(10),i==="magic"?o.setTint(10044671):o.clearTint(),o.setActive(!0).setVisible(!0),this.scene.anims.exists(c)?(o.play(c),o.once(u.Animations.Events.ANIMATION_COMPLETE,()=>{this.release(o)})):this.scene.tweens.add({targets:o,alpha:0,scaleX:n*1.5,scaleY:n*1.5,duration:200,onComplete:()=>this.release(o)})}playStatusLoop(t,e,s=-20){const i=this.acquire();if(!i)return null;const o=tt[e];return i.setScale(.5),i.setAlpha(.8),i.setDepth(15),i.setPosition(t.x,t.y+s),e==="magic"?i.setTint(10044671):i.clearTint(),i.setActive(!0).setVisible(!0),this.scene.anims.exists(o)&&i.play({key:o,repeat:-1}),i}release(t){t.stop(),t.setActive(!1).setVisible(!1),t.clearTint()}acquire(){for(const t of this.pool)if(!t.active)return t;if(this.pool.length>0){const t=this.pool[0];return this.release(t),t}return null}initPool(){for(let t=0;t0&&!this.scene.anims.exists(t)&&this.scene.anims.create({key:t,frames:i,frameRate:24,repeat:0})}this.animsRegistered=!0}}destroy(){for(const t of this.pool)t.destroy();this.pool.length=0}}const zs={physical:[8947848,6710886,11184810,4473924],fire:[16737792,16729088,16755200,16720384,13382400],ice:[8965375,11197951,6728430,13426175],lightning:[16777215,16777096,15658751,16777164],magic:[10044671,7807709,12281599,6689228],poison:[4521728,2280448,8978244,43520],stone:[8939076,6702114,11176038,4469538]},ve="__vfx_spark",Y="__vfx_soft",de="__vfx_pixel",me=class me{constructor(t,e){r(this,"scene");r(this,"layer");r(this,"activeEmitters",0);this.scene=t,this.layer=e,this.generateTextures()}isOnScreen(t,e,s=100){const i=this.scene.cameras.main;return t>=i.scrollX-s&&t<=i.scrollX+i.width+s&&e>=i.scrollY-s&&e<=i.scrollY+i.height+s}emitExplosion(t,e,s,i,o=0){if(!this.isOnScreen(t,e))return;const a=Rs(s,o,i),n=$(s),c=zs[i],m=20+n*80;this.emitBurst(t,e,Y,{quantity:Math.max(3,Math.round(a*.05)),speed:{min:200,max:400+n*300},lifespan:150,scale:{start:.3,end:1.5},alpha:{start:.8,end:0},tint:16777215,gravityY:0}),this.emitBurst(t,e,de,{quantity:Math.round(a*.3),speed:{min:100,max:300+n*200},lifespan:{min:300,max:600},scale:{start:.5+n*.5,end:.1},alpha:{start:1,end:.3},tint:c,gravityY:300,angle:{min:0,max:360},rotate:{min:-180,max:180}}),["fire","magic","poison","lightning"].includes(i)?this.emitBurst(t,e,Y,{quantity:Math.round(a*.2),speed:{min:20,max:60+n*40},lifespan:{min:400,max:800},scale:{start:.8+n,end:2+n*2},alpha:{start:.6,end:0},tint:c,gravityY:-50}):this.emitBurst(t,e,de,{quantity:Math.round(a*.2),speed:{min:80,max:200+n*150},lifespan:{min:400,max:700},scale:{start:.3+n*.4,end:.1},alpha:{start:1,end:.2},tint:c,gravityY:400,angle:{min:220,max:320}}),this.emitBurst(t,e,Y,{quantity:Math.round(a*.25),speed:{min:10,max:30+n*20},lifespan:{min:800,max:1500},scale:{start:.5,end:2+n*2},alpha:{start:.35,end:0},tint:i==="fire"?[3355443,2236962]:c,gravityY:-30,emitZone:{source:new u.Geom.Circle(0,0,m*.3),type:"random"}}),this.emitBurst(t,e,ve,{quantity:Math.round(a*.2),speed:{min:5,max:40+n*30},lifespan:{min:1e3,max:2e3},scale:{start:.3,end:.1},alpha:{start:.9,end:0},tint:c,gravityY:20,emitZone:{source:new u.Geom.Circle(0,0,m),type:"random"}})}emitPuff(t,e,s,i,o=400){this.isOnScreen(t,e)&&this.emitBurst(t,e,Y,{quantity:s,speed:{min:20,max:80},lifespan:o,scale:{start:.4,end:1.2},alpha:{start:.6,end:0},tint:i,gravityY:-20})}emitBurst(t,e,s,i){if(this.activeEmitters>=me.MAX_EMITTERS)return;const{quantity:o,...a}=i,n=this.scene.add.particles(t,e,s,{...a,frequency:-1,emitting:!1});n.setDepth(12),this.layer.add(n),n.explode(o,0,0),this.activeEmitters++;const c=i.lifespan;let m;typeof c=="object"&&c!==null&&"max"in c?m=c.max:typeof c=="number"?m=c:m=2e3,this.scene.time.delayedCall(m+200,()=>{n.destroy(),this.activeEmitters--})}generateTextures(){const t=this.scene.textures;if(!t.exists(ve)){const e=this.scene.make.graphics({x:0,y:0});e.fillStyle(16777215,1),e.fillCircle(4,4,4),e.generateTexture(ve,8,8),e.destroy()}if(!t.exists(Y)){const e=this.scene.make.graphics({x:0,y:0});e.fillStyle(16777215,.8),e.fillCircle(8,8,8),e.fillStyle(16777215,.4),e.fillCircle(8,8,6),e.generateTexture(Y,16,16),e.destroy()}if(!t.exists(de)){const e=this.scene.make.graphics({x:0,y:0});e.fillStyle(16777215,1),e.fillRect(0,0,3,3),e.generateTexture(de,3,3),e.destroy()}}destroy(){}};r(me,"MAX_EMITTERS",60);let Me=me;const st={physical:16777215,fire:16737792,ice:8965375,lightning:16777130,magic:10044671,poison:4521728,stone:11176038},Fs=.4,Ws=.6,it={burn:"fire",freeze:"ice",slow:"ice",stun:"lightning",poison:"poison"},js=[{statusA:"freeze",statusB:"stun",reaction:{name:"Shatter Blast",colors:[11197951,13426175,16777215,8961006],flashColor:13426175,damage:200,element:"ice"}},{statusA:"freeze",statusB:"burn",reaction:{name:"Thermal Shock",colors:[16777215,14540287,16746564,11193599],flashColor:16777215,damage:250,element:"fire"}},{statusA:"poison",statusB:"burn",reaction:{name:"Acid Melt",colors:[4521728,8978244,16737792,16755200],flashColor:8978244,damage:220,element:"poison"}},{statusA:"slow",statusB:"stun",reaction:{name:"Amplified Shock",colors:[16777130,16777096,16777215,16772676],flashColor:16777130,damage:180,element:"lightning"}},{statusA:"poison",statusB:"stun",reaction:{name:"Overcharge",colors:[4521728,11206468,16777130,8978312],flashColor:11206536,damage:200,element:"lightning"}}];function Ns(d){for(const t of js)if(d.includes(t.statusA)&&d.includes(t.statusB))return t.reaction;return null}const Hs={burn:[16737792,16729088,16755200],freeze:[8965375,11197951,13426175],slow:[10044671,7807709,12281599],stun:[16777130,16777096,16777215],poison:[4521728,2280448,8978244]};class Gs{constructor(t,e){r(this,"scene");r(this,"spritePool");r(this,"particles");r(this,"decals");r(this,"cameraFX");r(this,"statusOverlays",new Map);r(this,"onDamageDealt",t=>{const e=$(t.damage);e<.2?this.particles.emitPuff(t.x,t.y,5+Math.round(e*20),16777215,200):(this.spritePool.playImpact(t.x,t.y,t.damage,"physical"),this.particles.emitPuff(t.x,t.y,10+Math.round(e*30),16777215,300)),t.isCrit&&this.cameraFX.flash(16777215,.2,80)});r(this,"onSplashDamage",t=>{this.playImpact({x:t.x,y:t.y,element:"stone",damage:t.damage,isCrit:!1,isAoE:!0,isMiss:!1,splashRadius:t.radius})});r(this,"onEnemyDied",t=>{t.enemyId,t.reward,t.hp,this.playDeathEffect(t.x,t.y)});r(this,"onBossDied",t=>{t.bossId,t.reward,this.playBossDeath(t.x,t.y)});r(this,"onBossPhaseChanged",t=>{t.bossId,t.description,t.abilities;const e=this.scene.cameras.main;this.playBossPhaseTransition(e.centerX,e.centerY)});r(this,"onStatusApplied",t=>{t.value,t.duration,this.attachStatusOverlay(t.type,t.enemy,t.x,t.y);const e=this.statusOverlays.get(t.enemy);if(e&&e.size>=2){const s=Array.from(e.keys()),i=Ns(s);i&&this.playSynergyReaction(i,t.x,t.y)}});r(this,"onStatusExpired",t=>{this.removeStatusOverlay(t.type,t.enemy)});r(this,"onEnemyCleanup",t=>{this.cleanupAllOverlays(t.enemy)});this.scene=t,this.spritePool=new Vs(t,e),this.particles=new Me(t,e),this.decals=new Cs(t,e),this.cameraFX=new Os(t),this.setupListeners()}playImpact(t){const e=$(t.damage);if(this.spritePool.playImpact(t.x,t.y,t.damage,t.element),this.particles.emitExplosion(t.x,t.y,t.damage,t.element,t.splashRadius),(e>=.3||t.isAoE||t.isMiss)&&this.decals.spawn(t.x,t.y,t.damage,t.element),e>=Fs&&this.cameraFX.shake(t.damage),e>=Ws||t.isCrit){const s=st[t.element];this.cameraFX.flash(s,.15+e*.2,100)}l.emit("vfx:impact-played",{element:t.element,damage:t.damage,x:t.x,y:t.y})}playDeathEffect(t,e,s="physical"){this.particles.emitPuff(t,e,15,st[s]??16777215,500),this.spritePool.playImpact(t,e,50,s)}playBossPhaseTransition(t,e){this.cameraFX.flash(16777215,.4,200),this.cameraFX.shake(400),this.particles.emitExplosion(t,e,500,"magic",100)}playBossDeath(t,e){this.cameraFX.flash(16777215,.6,300),this.cameraFX.shake(800),this.particles.emitExplosion(t,e,1e3,"fire",120),this.spritePool.playImpact(t,e,1e3,"fire"),this.scene.time.delayedCall(300,()=>{this.cameraFX.slowMotion(.3,1500)}),this.scene.time.delayedCall(500,()=>{this.particles.emitExplosion(t,e,800,"magic",100),this.decals.spawn(t,e,1e3,"fire")})}playSynergyReaction(t,e,s){this.particles.emitExplosion(e,s,t.damage,t.element,60),this.spritePool.playImpact(e,s,t.damage,t.element),this.cameraFX.flash(t.flashColor,.35,150),this.cameraFX.shake(t.damage),this.decals.spawn(e,s,t.damage,t.element);const i=t.colors[0]??16777215;this.particles.emitPuff(e,s,20,i,700),l.emit("vfx:synergy-played",{name:t.name,element:t.element})}update(t){this.decals.update(t),this.updateStatusOverlays()}updateStatusOverlays(){const t=[];for(const[e,s]of this.statusOverlays){const i=e;if(!i.active){t.push(e);continue}for(const o of s.values())o.sprite&&o.sprite.active&&o.sprite.setPosition(i.x,i.y-20)}for(const e of t)this.cleanupAllOverlays(e)}attachStatusOverlay(t,e,s,i){let o=this.statusOverlays.get(e);if(o||(o=new Map,this.statusOverlays.set(e,o)),o.has(t))return;const a=Hs[t]??16777215;let n=null;switch(t){case"burn":{const c=it[t]??"fire";n=this.spritePool.playStatusLoop(e,c,-20),this.particles.emitPuff(s,i,8,a,500);break}case"freeze":{const c=e.bodySprite;c&&c.setTint(8965375),this.spritePool.playImpact(s,i,30,"ice"),this.particles.emitPuff(s,i,10,a,600);break}case"slow":{n=this.spritePool.playStatusLoop(e,"ice",-20),n&&(n.setTint(10044671),n.setScale(.3),n.setAlpha(.6));const c=e.bodySprite;c&&c.setTint(12290303),this.particles.emitPuff(s,i,6,a,800);break}case"stun":{this.particles.emitPuff(s,i-15,12,a,600);break}case"poison":{const c=it[t]??"poison";n=this.spritePool.playStatusLoop(e,c,-15),n&&n.setScale(.3),this.particles.emitPuff(s,i,8,a,500);break}default:this.particles.emitPuff(s,i,6,16777215,400);break}o.set(t,{sprite:n,target:e,statusType:t})}removeStatusOverlay(t,e){const s=this.statusOverlays.get(e);if(!s)return;const i=s.get(t);if(i){if(i.sprite&&this.spritePool.release(i.sprite),t==="freeze"||t==="slow"){const o=e.bodySprite;o&&o.clearTint()}s.delete(t),s.size===0&&this.statusOverlays.delete(e)}}cleanupAllOverlays(t){const e=this.statusOverlays.get(t);if(!e)return;for(const i of e.values())i.sprite&&this.spritePool.release(i.sprite);const s=t.bodySprite;s&&s.clearTint(),e.clear(),this.statusOverlays.delete(t)}setupListeners(){l.on(p.DAMAGE_DEALT,this.onDamageDealt,this),l.on(p.PROJECTILE_SPLASH,this.onSplashDamage,this),l.on(p.ENEMY_DIED,this.onEnemyDied,this),l.on(p.BOSS_DIED,this.onBossDied,this),l.on(p.BOSS_PHASE_CHANGED,this.onBossPhaseChanged,this),l.on("vfx:status-applied",this.onStatusApplied,this),l.on("vfx:status-expired",this.onStatusExpired,this),l.on("vfx:enemy-cleanup",this.onEnemyCleanup,this)}destroy(){l.off(p.DAMAGE_DEALT,this.onDamageDealt,this),l.off(p.PROJECTILE_SPLASH,this.onSplashDamage,this),l.off(p.ENEMY_DIED,this.onEnemyDied,this),l.off(p.BOSS_DIED,this.onBossDied,this),l.off(p.BOSS_PHASE_CHANGED,this.onBossPhaseChanged,this),l.off("vfx:status-applied",this.onStatusApplied,this),l.off("vfx:status-expired",this.onStatusExpired,this),l.off("vfx:enemy-cleanup",this.onEnemyCleanup,this);for(const t of this.statusOverlays.values())for(const e of t.values())e.sprite&&this.spritePool.release(e.sprite);this.statusOverlays.clear(),this.spritePool.destroy(),this.particles.destroy(),this.decals.destroy(),this.cameraFX.destroy()}}class nt extends u.Scene{constructor(e="GameScene"){super({key:e});r(this,"sceneManager");r(this,"waveManager");r(this,"towerSystem");r(this,"enemySystem");r(this,"bossSystem");r(this,"abilitySystem");r(this,"comboSystem");r(this,"dropSystem");r(this,"floatingText");r(this,"achievementSystem");r(this,"audioSystem");r(this,"uiAudio");r(this,"vfxAudioBridge");r(this,"musicController");r(this,"vfxManager");r(this,"maxCombo",0);r(this,"totalKills",0);r(this,"hud");r(this,"towerPanel");r(this,"wavePanel");r(this,"bossHealthBar");r(this,"upgradeMenu");r(this,"radiusOverlay");r(this,"gold",150);r(this,"lives",20);r(this,"currentWave",0);r(this,"totalWaves",0);r(this,"isPaused",!1);r(this,"waves",[]);r(this,"levelStarConditions",[]);r(this,"map");r(this,"pathPoints",[]);r(this,"towerLayer");r(this,"enemyLayer");r(this,"projectileLayer");r(this,"effectLayer")}create(e){this.sceneManager=new ee(this),_.currentWorldId=e.worldId,_.currentLevelId=e.levelId,this.gold=_.gold=150,this.lives=_.lives=20,this.towerLayer=this.add.layer(),this.enemyLayer=this.add.layer(),this.projectileLayer=this.add.layer(),this.effectLayer=this.add.layer(),this.setupMap(e),this.pathPoints=this.map?X.extractPathFromMap(this.map):this.buildFallbackPath(),this.drawPathVisual(),H.registerAnimations(this),he.registerAnimations(this),this.enemySystem=new Ve(this,this.pathPoints),this.enemySystem.setupListeners(),this.bossSystem=new ze(this,this.enemySystem),this.bossSystem.setupListeners(),this.towerSystem=new q(this),this.hud=new Wt(this,0,this.gold,this.lives),this.towerPanel=new Nt(this,this.towerSystem),this.wavePanel=new Ht(this),this.upgradeMenu=new Ie(this),this.radiusOverlay=new Ut(this),this.bossHealthBar=new Oe(this),this.applyWorldTint(e.worldId),this.setupInput(),this.registerEvents(),this.cameras.main.fadeIn(400,0,0,0),this.loadLevelAndInit(e)}async loadLevelAndInit(e){const s=await ke.loadLevel(e.levelId);if(!this.sys.isActive())return;if((s==null?void 0:s.startGold)!=null&&(this.gold=_.gold=s.startGold,this.hud.updateGold(this.gold)),(s==null?void 0:s.startLives)!=null&&(this.lives=_.lives=s.startLives,this.hud.updateLives(this.lives)),this.levelStarConditions=(s==null?void 0:s.stars)??[],this.waves=(s==null?void 0:s.waves)??this.buildPlaceholderWaves(e.worldId,e.isBoss??!1),this.totalWaves=this.waves.length,this.waveManager=new bt(this,this.waves,this.pathPoints),this.hud.updateWave(0,this.totalWaves),e.worldId>=21){this.abilitySystem=new qt(this,e.worldId);const o=this.abilitySystem.getAbilities();this.hud.buildAbilityBar(o),l.on("ability:charges-changed",a=>{this.hud.updateAbilityCharges(a,this.abilitySystem.getAbilities())},this)}this.floatingText=new ys(this),this.comboSystem=new is(this),this.dropSystem=new ms(this),this.hud.buildExpBar(e.worldId>=21),this.achievementSystem=new vs(this,this.floatingText),this.audioSystem=new Ss(this),this.uiAudio=new Ts,this.vfxAudioBridge=new Ps,this.musicController=new _t(this),this.musicController.play(gt(e.worldId)),Ls({low:.3,medium:.6,high:1}[_.settings.quality]??1),this.vfxManager=new Gs(this,this.effectLayer),this.registerCombatEvents(),this.time.delayedCall(2e3,()=>{var o;return(o=this.waveManager)==null?void 0:o.startNextWave()})}setupMap(e){const s=`map-${e.worldKey}-${e.levelId}`;if(this.cache.tilemap.has(s)){this.map=this.make.tilemap({key:s});const i=this.map.addTilesetImage("tileset",`ts-${e.worldKey}`);i&&(this.map.createLayer("Ground",i,0,0),this.map.createLayer("Decoration",i,0,0))}else{const{width:i,height:o}=this.cameras.main;if(this.textures.exists("world-01-bg")){const a=this.add.image(0,0,"world-01-bg").setOrigin(0,0);a.setDisplaySize(i,o),a.setDepth(-10)}else{const a=this.add.graphics();a.fillStyle(1714714,1),a.fillRect(0,0,i,o),a.lineStyle(1,2767402,.4);for(let n=0;nthis.togglePause()),(s=this.input.keyboard)==null||s.addKey(u.Input.Keyboard.KeyCodes.P).on("down",()=>this.togglePause())}registerEvents(){this.events.on("ui:send-wave",()=>{var e;this.isPaused||(e=this.waveManager)==null||e.startNextWave()}),this.events.on("ui:toggle-pause",()=>this.togglePause()),this.events.on("ui:speed-toggle",e=>{this.time.timeScale=e}),l.on(p.GOLD_CHANGED,e=>{this.gold=Math.max(0,this.gold+e),_.gold=this.gold,this.hud.updateGold(this.gold)},this),l.on(p.LIVES_CHANGED,e=>{this.lives+=e,_.lives=this.lives,this.hud.updateLives(this.lives),this.lives<=0&&this.time.delayedCall(800,()=>this.sceneManager.transitionTo("DefeatScene",{worldId:_.currentWorldId,levelId:_.currentLevelId,waveReached:this.currentWave,totalWaves:this.totalWaves}))},this),l.on(p.WAVE_STARTED,e=>{var s;this.currentWave=e.wave,this.hud.updateWave(this.currentWave,this.totalWaves),this.hud.setWaveButtonVisible(!1),this.wavePanel.showWaveActive(this.currentWave),this.currentWave===this.totalWaves&&this.totalWaves>1&&((s=this.musicController)==null||s.hardCut("bossA"))},this),l.on(p.WAVE_COMPLETED,e=>{var s;if(l.emit(p.GOLD_CHANGED,e.reward),this.currentWave{this.hud.setWaveButtonVisible(!1),this.wavePanel.showVictoryPending(),this.time.delayedCall(1500,()=>this.sceneManager.transitionTo("VictoryScene",{worldId:_.currentWorldId,levelId:_.currentLevelId,gold:this.gold,lives:this.lives,maxCombo:this.maxCombo,totalKills:this.totalKills,stars:this.evaluateStars(this.levelStarConditions,this.lives)}))},this),l.on(p.TOWER_SELECTED,e=>{var s,i,o;(s=e==null?void 0:e.placed)!=null&&s.tower&&((i=this.upgradeMenu)==null||i.open(e.placed.tower),(o=this.radiusOverlay)==null||o.show(e.placed.tower.x,e.placed.tower.y,e.placed.tower.getRange()))},this),l.on(p.TOWER_DESELECTED,()=>{var e,s;(e=this.upgradeMenu)==null||e.close(),(s=this.radiusOverlay)==null||s.hide()},this),l.on("boss:spawned",e=>{var s;(s=this.bossHealthBar)==null||s.activate(e.bossName,e.maxHp)},this),l.on("boss:hp-update",e=>{var s;(s=this.bossHealthBar)==null||s.setHP(e.hp)},this),l.on(p.TOWER_SOLD,e=>{var s;if(e&&"tower"in e&&e.tower){const i=this.towerSystem;(s=i.removeTowerBySprite)==null||s.call(i,e.tower)}},this)}evaluateStars(e,s){for(let i=0;i=(\d+)$/);n&&(a=s>=parseInt(n[1],10))}if(a)return Math.min(3,e.length-i)}return 0}registerCombatEvents(){l.on("combo:updated",e=>{if(this.floatingText&&(e.count>this.maxCombo&&(this.maxCombo=e.count),e.count>=5)){const s=e.multiplier>1?`×${e.count} COMBO! ×${e.multiplier.toFixed(1)}`:`×${e.count} COMBO!`;this.floatingText.show(e.x,e.y-20,s,"combo")}},this),l.on("combo:fanfare",e=>{if(!this.floatingText)return;const s={10:"🔥 ON FIRE!",25:"⚡ UNSTOPPABLE!",50:"💥 LEGENDARY!!"};this.floatingText.show(e.x,e.y,s[e.milestone]??"INCREDIBLE!","fanfare")},this),l.on("combo:gold-bonus",e=>{var s;this.gold=Math.max(0,this.gold+e.bonus),_.gold=this.gold,this.hud.updateGold(this.gold),(s=this.floatingText)==null||s.show(e.x,e.y-40,`+${e.bonus}💰`,"gold")},this),l.on("exp:updated",e=>{this.hud.updateExpBar(e.percent)},this),l.on("exp:charge-earned",()=>{var e;this.events.emit("exp:charge-earned"),(e=this.floatingText)==null||e.show(this.cameras.main.centerX,this.cameras.main.centerY+60,"✨ ABILITY CHARGE!","powerup")},this),l.on("drop:gold",e=>{l.emit(p.GOLD_CHANGED,e.amount)},this),l.on("drop:heal",e=>{var s;l.emit(p.LIVES_CHANGED,e.amount),(s=this.floatingText)==null||s.show(this.cameras.main.centerX,this.cameras.main.centerY,`+${e.amount} ❤️`,"heal")},this),l.on("drop:speed-boost",e=>{var s;l.emit("ability:tower-surge",{value:1.5,duration:e.duration}),(s=this.floatingText)==null||s.show(this.cameras.main.centerX,this.cameras.main.centerY,"⚡ SPEED BOOST!","powerup")},this),l.on("drop:shield",()=>{var e;l.emit(p.LIVES_CHANGED,1),(e=this.floatingText)==null||e.show(this.cameras.main.centerX,this.cameras.main.centerY,"🛡️ SHIELD!","powerup")},this),l.on("drop:bomb",()=>{var e;l.emit("ability:aoe-damage",{damage:200}),(e=this.floatingText)==null||e.show(this.cameras.main.centerX,this.cameras.main.centerY,"💣 BOOM!","crit")},this),l.on(p.DAMAGE_DEALT,e=>{if(!this.floatingText)return;const s=e.isCrit?`${e.damage}!`:String(e.damage);this.floatingText.show(e.x,e.y,s,e.isCrit?"crit":"damage")},this),l.on(p.ENEMY_DIED,()=>{this.totalKills+=1},this)}togglePause(){var e,s;this.isPaused=!this.isPaused,this.isPaused?(this.physics.pause(),(e=this.waveManager)==null||e.pause()):(this.physics.resume(),(s=this.waveManager)==null||s.resume())}applyWorldTint(e){const i={1:8956552,2:14527095,3:8956637,4:14518323,5:6719590,6:10044620,7:16777215,8:8952217,9:3364266,10:11167283,11:4460868,12:7838122,13:11176004,14:2271948,15:7838156,16:13386786,17:7812010,18:13408563,19:4456550,20:6706602,21:4504388,22:13408597,23:10066380,24:10053375,25:2250154,26:1122884,27:3359846,28:13382417,29:1114146}[e];if(i!==void 0&&i!==16777215){const{width:o,height:a}=this.cameras.main,n=this.add.rectangle(o/2,a/2,o,a,i,.18);n.setScrollFactor(0),n.setDepth(0)}}drawPathVisual(){if(!this.textures.exists("road-tile-1"))return;const e=48;for(let s=0;si===0?e.moveTo(s.x,s.y):e.lineTo(s.x,s.y)),e.strokePath(),this.pathPoints.forEach((s,i)=>{e.fillStyle(16737792,.9),e.fillCircle(s.x,s.y,7),this.add.text(s.x+10,s.y-10,String(i),{fontSize:"10px",color:"#ff6600"})})}buildPlaceholderWaves(e,s){const i=e*1.3,o=s?1:5;return Array.from({length:o},(a,n)=>({waveNumber:n+1,spawnGroups:[{enemy:`enemy-${String(Math.min(e*2,40)).padStart(2,"0")}`,count:Math.floor(5+n*3*i),interval:Math.max(500,1200-n*100),delay:0}],reward:Math.floor(50+n*15*i),stars:[]}))}update(e,s){var i;this.isPaused||(this.enemySystem.update(e,s),this.bossSystem.update(e,s),this.towerSystem.update(e,s,this.enemySystem.getEnemiesByProgress()),(i=this.vfxManager)==null||i.update(s))}shutdown(){var e,s,i,o,a,n,c,m,f,g,y,w,E,T;this.hud.destroy(),this.towerPanel.destroy(),this.wavePanel.destroy(),(e=this.upgradeMenu)==null||e.destroy(),(s=this.radiusOverlay)==null||s.destroy(),(i=this.bossHealthBar)==null||i.destroy(),this.towerSystem.destroy(),this.enemySystem.destroy(),(o=this.waveManager)==null||o.destroy(),(a=this.abilitySystem)==null||a.destroy(),(n=this.comboSystem)==null||n.destroy(),(c=this.dropSystem)==null||c.destroy(),(m=this.floatingText)==null||m.destroy(),(f=this.achievementSystem)==null||f.destroy(),(g=this.audioSystem)==null||g.destroy(),(y=this.uiAudio)==null||y.destroy(),(w=this.vfxAudioBridge)==null||w.destroy(),(E=this.musicController)==null||E.destroy(),(T=this.vfxManager)==null||T.destroy(),this.events.off("ui:send-wave"),this.events.off("ui:toggle-pause"),this.events.off("ui:speed-toggle"),l.off(p.GOLD_CHANGED,void 0,this),l.off(p.LIVES_CHANGED,void 0,this),l.off(p.WAVE_STARTED,void 0,this),l.off(p.WAVE_COMPLETED,void 0,this),l.off(p.ALL_WAVES_COMPLETED,void 0,this),l.off("ability:charges-changed",void 0,this),l.off("combo:updated",void 0,this),l.off("combo:fanfare",void 0,this),l.off("combo:gold-bonus",void 0,this),l.off("exp:updated",void 0,this),l.off("exp:charge-earned",void 0,this),l.off("drop:gold",void 0,this),l.off("drop:heal",void 0,this),l.off("drop:speed-boost",void 0,this),l.off("drop:shield",void 0,this),l.off("drop:bomb",void 0,this),l.off(p.DAMAGE_DEALT,void 0,this),l.off(p.ENEMY_DIED,void 0,this),l.off(p.TOWER_SELECTED,void 0,this),l.off(p.TOWER_DESELECTED,void 0,this),l.off("boss:spawned",void 0,this),l.off("boss:hp-update",void 0,this),l.off(p.TOWER_SOLD,void 0,this)}}function Us(d){return d>=15?3:d>=8?2:d>=1?1:0}class $s extends u.Scene{constructor(){super({key:"VictoryScene"})}create(t){const{width:e,height:s}=this.cameras.main,i=new ee(this),o=`world-${String(t.worldId).padStart(2,"0")}`,a=t.stars??Us(t.lives);_.completeLevel(o,t.levelId,a,0),Fe("victory"),this.add.rectangle(e/2,s/2,e,s,662026);for(let w=0;w<60;w++){const E=u.Math.Between(0,e),T=u.Math.Between(0,s),x=u.Math.Between(1,3),S=this.add.circle(E,T,x,16768324,.6);this.tweens.add({targets:S,alpha:{from:.1,to:.9},duration:u.Math.Between(700,2200),yoyo:!0,repeat:-1,delay:u.Math.Between(0,2e3)})}this.time.delayedCall(200,()=>{for(let w=0;w<30;w++){const E=Math.PI*2/30*w,T=u.Math.Between(60,220),x=e/2+Math.cos(E)*5,S=s*.32+Math.sin(E)*5,R=this.add.circle(x,S,u.Math.Between(3,7),16768324,1);this.tweens.add({targets:R,x:x+Math.cos(E)*T,y:S+Math.sin(E)*T,alpha:0,scale:.2,duration:u.Math.Between(600,1100),ease:"Quad.easeOut",onComplete:()=>R.destroy()})}});const n=this.add.text(e/2,s*.18,"VICTORY!",{fontSize:"72px",color:"#ffdd44",fontFamily:"sans-serif",fontStyle:"bold",stroke:"#aa8800",strokeThickness:6}).setOrigin(.5).setScale(0).setAlpha(0);this.tweens.add({targets:n,scale:1,alpha:1,duration:500,ease:"Back.easeOut"});const c=s*.36,m=70;for(let w=0;w<3;w++){const E=w{this.tweens.add({targets:x,scaleX:1.3,scaleY:.8,duration:80,yoyo:!0,ease:"Quad.easeOut"})})}this.add.text(e/2,s*.5,`💰 Gold: ${t.gold} ❤️ Lives: ${t.lives}`,{fontSize:"22px",color:"#aaffaa",fontFamily:"monospace"}).setOrigin(.5).setAlpha(0),this.tweens.add({targets:this.children.getAt(this.children.length-1),alpha:1,duration:400,delay:1400});const f=(t.maxCombo??0)>=5,g=(t.totalKills??0)>0;if(f||g){const w=[];g&&w.push(`⚔️ Kills: ${t.totalKills}`),f&&w.push(`🔥 Best Combo: ×${t.maxCombo}`);const E=this.add.text(e/2,s*.58,w.join(" "),{fontSize:"18px",color:"#88ccff",fontFamily:"monospace"}).setOrigin(.5).setAlpha(0);this.tweens.add({targets:E,alpha:1,duration:400,delay:1700})}const y=s*.74;this.time.delayedCall(1800,()=>{this.makeBtn(e/2-140,y,"RETRY",3355443,"#aaaaaa",()=>i.transitionTo("GameScene",{worldId:t.worldId,worldKey:o,levelId:t.levelId})),this.makeBtn(e/2,y,"NEXT LEVEL",2245666,"#88ff88",()=>i.transitionTo("LevelSelectScene",{worldId:t.worldId,worldKey:o})),this.makeBtn(e/2+145,y,"MENU",2236996,"#8888ff",()=>i.transitionTo("MainMenuScene"))})}makeBtn(t,e,s,i,o,a){const n=this.add.rectangle(t,e,200,48,i).setStrokeStyle(1,8947848).setInteractive({useHandCursor:!0});this.add.text(t,e,s,{fontSize:"18px",color:o,fontFamily:"monospace"}).setOrigin(.5);const c=m=>u.Display.Color.ValueToColor(m).darken(15).color;n.on("pointerover",()=>n.setFillStyle(c(i))),n.on("pointerout",()=>n.setFillStyle(i)),n.on("pointerup",a)}}class Ks extends u.Scene{constructor(){super({key:"DefeatScene"})}create(t){const{width:e,height:s}=this.cameras.main,i=new ee(this),o=`world-${String(t.worldId).padStart(2,"0")}`;this.cameras.main.shake(600,.012),Fe("defeat"),this.add.rectangle(e/2,s/2,e,s,1639685);for(let c=0;c<25;c++){const m=u.Math.Between(0,e),f=u.Math.Between(-40,s*.4),g=this.add.circle(m,f,u.Math.Between(2,5),16729088,.85);u.Math.Between(60,180),this.tweens.add({targets:g,y:f+u.Math.Between(200,s),alpha:0,duration:u.Math.Between(1200,2800),ease:"Quad.easeIn",delay:u.Math.Between(0,1500),repeat:-1,onRepeat:()=>{g.setPosition(u.Math.Between(0,e),u.Math.Between(-40,0)),g.setAlpha(.85)},x:m+u.Math.Between(-30,30)})}const a=this.add.rectangle(e/2,s/2,e,s,16711680,.35);this.tweens.add({targets:a,alpha:0,duration:1e3,ease:"Quad.easeOut"});const n=this.add.text(e/2,s*.24,"DEFEATED",{fontSize:"72px",color:"#ff4444",fontFamily:"sans-serif",fontStyle:"bold",stroke:"#880000",strokeThickness:6}).setOrigin(.5).setAlpha(0).setScale(1.4);if(this.tweens.add({targets:n,alpha:1,scale:1,duration:400,ease:"Quad.easeOut",onComplete:()=>{this.tweens.add({targets:n,scaleX:1.04,scaleY:1.04,duration:700,yoyo:!0,repeat:-1,ease:"Sine.easeInOut"})}}),this.add.text(e/2,s*.4,"The enemies have broken through!",{fontSize:"20px",color:"#cc8888",fontFamily:"monospace"}).setOrigin(.5).setAlpha(0),this.tweens.add({targets:this.children.getAt(this.children.length-1),alpha:1,duration:400,delay:500}),t.waveReached!==void 0&&t.totalWaves!==void 0){const c=this.add.text(e/2,s*.5,`📊 Reached Wave ${t.waveReached} / ${t.totalWaves}`,{fontSize:"20px",color:"#ff9966",fontFamily:"monospace"}).setOrigin(.5).setAlpha(0);this.tweens.add({targets:c,alpha:1,duration:400,delay:750})}this.time.delayedCall(900,()=>{this.makeBtn(e/2-115,s*.66,"RETRY",8921634,"#ff8888",()=>i.transitionTo("GameScene",{worldId:t.worldId,worldKey:o,levelId:t.levelId})),this.makeBtn(e/2+115,s*.66,"MAIN MENU",2236996,"#8888ff",()=>i.transitionTo("MainMenuScene"))})}makeBtn(t,e,s,i,o,a){const n=this.add.rectangle(t,e,200,50,i).setStrokeStyle(1,8947848).setInteractive({useHandCursor:!0});this.add.text(t,e,s,{fontSize:"20px",color:o,fontFamily:"monospace"}).setOrigin(.5);const c=m=>u.Display.Color.ValueToColor(m).darken(15).color;n.on("pointerover",()=>n.setFillStyle(c(i))),n.on("pointerout",()=>n.setFillStyle(i)),n.on("pointerup",a)}}class Ys extends u.Scene{constructor(){super({key:"SettingsScene"})}create(){const{width:t,height:e}=this.cameras.main;this.add.rectangle(t/2,e/2,t,e,0,.85),this.add.rectangle(t/2,e/2,480,420,1710638).setStrokeStyle(2,8947967),this.add.text(t/2,e/2-170,"SETTINGS",{fontSize:"36px",color:"#ffdd44",fontFamily:"sans-serif",fontStyle:"bold"}).setOrigin(.5),this.add.text(t/2-180,e/2-80,"Music Volume",{fontSize:"20px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(0,.5);const s=Math.round(_.settings.musicVolume*100),i=this.add.text(t/2+100,e/2-80,`${s}%`,{fontSize:"20px",color:"#ffdd44",fontFamily:"monospace"}).setOrigin(.5);this.createSmallBtn(t/2+50,e/2-80,"-",()=>{_.settings.musicVolume=Math.max(0,_.settings.musicVolume-.1),i.setText(`${Math.round(_.settings.musicVolume*100)}%`),_.save(),l.emit(p.SET_MUSIC_VOLUME,{volume:_.settings.musicVolume})}),this.createSmallBtn(t/2+150,e/2-80,"+",()=>{_.settings.musicVolume=Math.min(1,_.settings.musicVolume+.1),i.setText(`${Math.round(_.settings.musicVolume*100)}%`),_.save(),l.emit(p.SET_MUSIC_VOLUME,{volume:_.settings.musicVolume})}),this.add.text(t/2-180,e/2-20,"SFX Volume",{fontSize:"20px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(0,.5);const o=Math.round(_.settings.sfxVolume*100),a=this.add.text(t/2+100,e/2-20,`${o}%`,{fontSize:"20px",color:"#ffdd44",fontFamily:"monospace"}).setOrigin(.5);this.createSmallBtn(t/2+50,e/2-20,"-",()=>{_.settings.sfxVolume=Math.max(0,_.settings.sfxVolume-.1),a.setText(`${Math.round(_.settings.sfxVolume*100)}%`),_.save(),l.emit(p.SET_SFX_VOLUME,{volume:_.settings.sfxVolume})}),this.createSmallBtn(t/2+150,e/2-20,"+",()=>{_.settings.sfxVolume=Math.min(1,_.settings.sfxVolume+.1),a.setText(`${Math.round(_.settings.sfxVolume*100)}%`),_.save(),l.emit(p.SET_SFX_VOLUME,{volume:_.settings.sfxVolume})}),this.add.text(t/2-180,e/2+40,"Quality",{fontSize:"20px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(0,.5);const n=["low","medium","high"],c=this.add.text(t/2+100,e/2+40,_.settings.quality.toUpperCase(),{fontSize:"18px",color:"#ffdd44",fontFamily:"monospace"}).setOrigin(.5);this.createSmallBtn(t/2+50,e/2+40,"<",()=>{const g=n.indexOf(_.settings.quality);_.settings.quality=n[Math.max(0,g-1)],c.setText(_.settings.quality.toUpperCase()),_.save()}),this.createSmallBtn(t/2+150,e/2+40,">",()=>{const g=n.indexOf(_.settings.quality);_.settings.quality=n[Math.min(n.length-1,g+1)],c.setText(_.settings.quality.toUpperCase()),_.save()});const m=this.add.rectangle(t/2,e/2+110,220,44,8921634).setInteractive({useHandCursor:!0}).setStrokeStyle(2,11158596);this.add.text(t/2,e/2+110,"RESET PROGRESS",{fontSize:"16px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(.5),m.on("pointerup",()=>{_.reset(),this.add.text(t/2,e/2+145,"Progress reset!",{fontSize:"14px",color:"#ff4444",fontFamily:"sans-serif"}).setOrigin(.5)});const f=this.add.rectangle(t/2,e/2+170,200,48,2250137).setInteractive({useHandCursor:!0}).setStrokeStyle(2,4487099);this.add.text(t/2,e/2+170,"CLOSE",{fontSize:"22px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(.5),f.on("pointerover",()=>f.setFillStyle(3373004)),f.on("pointerout",()=>f.setFillStyle(2250137)),f.on("pointerup",()=>this.scene.stop()),this.input.on("pointerup",g=>{const y=Math.abs(g.x-t/2),w=Math.abs(g.y-e/2);(y>240||w>210)&&this.scene.stop()}),this.cameras.main.fadeIn(200,0,0,0)}createSmallBtn(t,e,s,i){const o=this.add.rectangle(t,e,36,36,3359846).setInteractive({useHandCursor:!0}).setStrokeStyle(1,8947967);return this.add.text(t,e,s,{fontSize:"20px",color:"#ffffff",fontFamily:"monospace"}).setOrigin(.5),o.on("pointerover",()=>o.setFillStyle(4478378)),o.on("pointerout",()=>o.setFillStyle(3359846)),o.on("pointerup",i),o}}class Xs extends nt{constructor(){super("EndlessScene")}create(t){super.create({worldId:(t==null?void 0:t.worldId)??7,worldKey:"world-07",levelId:"endless",isBoss:!1});const{width:e}=this.cameras.main;this.add.text(e/2,40,"ENDLESS MODE",{fontSize:"28px",color:"#ffaa44",fontFamily:"sans-serif",fontStyle:"bold"}).setOrigin(.5)}}const lt=64,qs=20,Qs=12,Js=lt*qs,Zs=lt*Qs,ei={type:u.AUTO,width:Js,height:Zs,parent:"game-container",backgroundColor:"#000000",scene:[ct,yt,wt,Et,St,nt,$s,Ks,Ys,Xs],physics:{default:"arcade",arcade:{debug:!1,gravity:{x:0,y:0}}},scale:{mode:u.Scale.FIT,autoCenter:u.Scale.CENTER_BOTH},audio:{disableWebAudio:!1}},ti=new u.Game(ei);window.__phaserGame=ti;window.__gameState=null; diff --git a/vps/patches/daveai-production-runtime-20260731/windsurf.html b/vps/patches/daveai-production-runtime-20260731/windsurf.html new file mode 100644 index 00000000000..d19784e47d0 --- /dev/null +++ b/vps/patches/daveai-production-runtime-20260731/windsurf.html @@ -0,0 +1,2375 @@ + + + + + + + DaveAI — Ops Control Panel + + + + + + +
⚠ ALERT: — click to dismiss
+ + +
+
+
VPS +
+
CPU
+
MEM
+
UPTIME
+
+ +
last poll –
+ + + +
+ + +
+ + + + + +
+
+
🚨 Monitor
+
⌨ Command
+
🖥 Procs
+
📄 Logs
+
🧪 Tests
+
🚀 Deploy
+
💬 Discuss
+
+ + +
+
+
+
+
+ Live Activity Feed + 0 events +
+
+
+
+
+
Budget Status
+
+
+
+
Checkpoint
+
+
+
+
+
+ + +
+
+
+ Shell via /tools/shell/run + Common: pm2 list + · pm2 logs · docker ps · df -h · free -m · uptime +
+
+
+ root@vps:~$ + + + +
+
+
+ + +
+
+
+
PM2 Processes
+ + + + + + + + + + + + + + +
NameStatusPIDRestartsCPUMemUptimeActions
+
+
+
Docker Containers
+ + + + + + + + + + +
NameStatusPortsActions
+
+
+
+ + +
+
+ Service: + + + + + + + + + +
+
+
+ + +
+
+ + + + + + +
Select a test to run
+ +
+
+
+
Perf
+
+
+
+
A11y
+
+
+
+
Best P
+
+
+
+
SEO
+
+
+
+
+
+ + +
+
+
+
+
🧪 Deploy → Staging
+
Push current codebase to staging environment. Tests run automatically before + promotion.
+ +
+
+
🚀 Deploy → Production
+
Promote to daveai.tech production. Requires staging to be green.
+ +
+
+
⏪ Rollback
+
Roll back to previous checkpoint. Lists recent deployments to choose from.
+ +
+
+
📊 Checkpoint Status
+
Loading...
+
💰 Budget Status
+
Loading...
+
+
+
+
Deploy Output
+
+
+
+
+ + +
+
+ Agents: + + + + + +
+
+
+ +
+ + +
+
+ Agent Chat + +
+
+
+ + +
+
+ +
+ + +
+ + + + + \ No newline at end of file diff --git a/vps/patches/deploy-accessibility-contract-20260725.sh b/vps/patches/deploy-accessibility-contract-20260725.sh new file mode 100644 index 00000000000..7a924b52463 --- /dev/null +++ b/vps/patches/deploy-accessibility-contract-20260725.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +candidate_dir="${DAVEAI_CANDIDATE_DIR:-/tmp/daveai-accessibility-20260725}" +candidate_html="$candidate_dir/daveai-ui-v6.html" +candidate_css="$candidate_dir/daveai-v6.css" +expected_html="a9dc0ebfb1e6e87f62329e642892e674a573bdddd70f8a0d5fd3408b826e3cff" +expected_css="bca841c9633c7ef3420865fbdf41872ef4cd4130f71004842aa084b4f2246392" +expected_cache_key="20260725-accessibility-1" + +test "$(sha256sum "$candidate_html" | awk '{print $1}')" = "$expected_html" +test "$(sha256sum "$candidate_css" | awk '{print $1}')" = "$expected_css" +grep -q "/assets/daveai-v6.css?v=$expected_cache_key" "$candidate_html" +grep -q 'id="fp-tool-count"' "$candidate_html" +grep -q 'function activateOnKeyboard(event)' "$candidate_html" + +stamp="$(date -u +%Y%m%dT%H%M%SZ)" +backup="/opt/daveai/backups/${stamp}-accessibility-contract" +mkdir -p "$backup" +cp -a /var/www/agentic-website/index.html "$backup/index.html" +cp -a /var/www/agentic-website/daveai-ui-v6.html "$backup/daveai-ui-v6.html" +cp -a /var/www/agentic-website/assets/daveai-v6.css "$backup/daveai-v6.css" + +installed=0 +rollback() { + if [ "$installed" -eq 1 ]; then + cp -a "$backup/index.html" /var/www/agentic-website/index.html + cp -a "$backup/daveai-ui-v6.html" /var/www/agentic-website/daveai-ui-v6.html + cp -a "$backup/daveai-v6.css" /var/www/agentic-website/assets/daveai-v6.css + fi +} +trap rollback ERR + +install -o root -g root -m 0644 "$candidate_html" /var/www/agentic-website/daveai-ui-v6.html +install -o www-data -g www-data -m 0644 "$candidate_html" /var/www/agentic-website/index.html +install -o www-data -g www-data -m 0644 "$candidate_css" /var/www/agentic-website/assets/daveai-v6.css +installed=1 + +test "$(sha256sum /var/www/agentic-website/index.html | awk '{print $1}')" = "$expected_html" +test "$(sha256sum /var/www/agentic-website/daveai-ui-v6.html | awk '{print $1}')" = "$expected_html" +test "$(sha256sum /var/www/agentic-website/assets/daveai-v6.css | awk '{print $1}')" = "$expected_css" +public_html="$(curl --fail --silent "https://daveai.tech/?release=$stamp")" +grep -q "/assets/daveai-v6.css?v=$expected_cache_key" <<<"$public_html" +grep -q 'id="fp-tool-count"' <<<"$public_html" +grep -q 'function activateOnKeyboard(event)' <<<"$public_html" +test "$(curl --fail --silent "https://daveai.tech/assets/daveai-v6.css?v=$expected_cache_key&release=$stamp" | sha256sum | awk '{print $1}')" = "$expected_css" +curl --fail --silent https://api.daveai.tech/health | grep -q '"tools":120' + +trap - ERR +printf 'backup=%s\n' "$backup" +printf 'html_sha256=%s\n' "$expected_html" +printf 'css_sha256=%s\n' "$expected_css" +printf 'cache_key=%s\n' "$expected_cache_key" +printf 'homepage=ok stylesheet=ok brain=ok accessibility_contract=ok\n' diff --git a/vps/production-e2e-runner.cjs b/vps/production-e2e-runner.cjs new file mode 100644 index 00000000000..2c8545366d9 --- /dev/null +++ b/vps/production-e2e-runner.cjs @@ -0,0 +1,262 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ + +const fs = require('node:fs'); +const path = require('node:path'); + +let chromium; +try { + ({ chromium } = require('playwright')); +} catch (error) { + console.error('Missing dependency: playwright. Install it or run with the Codex bundled NODE_PATH.'); + console.error(error.message); + process.exit(1); +} + +const base = process.env.DAVEAI_BASE_URL || 'https://daveai.tech'; +const proofDir = path.resolve(process.env.DAVEAI_PROOF_DIR || path.join('proofs', `daveai-prod-e2e-${Date.now()}`)); +const authEmail = process.env.DAVEAI_E2E_EMAIL || ''; +const authPassword = process.env.DAVEAI_E2E_PASSWORD || ''; + +fs.mkdirSync(proofDir, { recursive: true }); + +const results = []; + +function record(name, status, details = {}) { + results.push({ name, status, ...details }); + const label = status === 'passed' ? 'PASS' : status === 'skipped' ? 'SKIP' : 'FAIL'; + console.log(`${label} ${name}`); +} + +async function newTrackedPage(browser, name) { + const page = await browser.newPage({ viewport: { width: 1365, height: 768 }, ignoreHTTPSErrors: true }); + const trace = { badResponses: [], requestFailures: [], pageErrors: [], consoleMessages: [] }; + page.on('response', (response) => { + const status = response.status(); + if (status >= 400) trace.badResponses.push({ status, url: response.url() }); + }); + page.on('requestfailed', (request) => { + trace.requestFailures.push({ + url: request.url(), + method: request.method(), + resourceType: request.resourceType(), + failure: request.failure()?.errorText || '', + }); + }); + page.on('pageerror', (error) => trace.pageErrors.push(String(error.message || error))); + page.on('console', (message) => { + const text = message.text(); + if (/Failed to load resource|error|exception|uncaught|not found|502|404|401|ERR_FAILED/i.test(text)) { + trace.consoleMessages.push({ type: message.type(), text, location: message.location() }); + } + }); + page._daveTrace = trace; + page._proofName = name; + return page; +} + +function assertCleanTrace(trace, allow = () => false) { + const badResponses = trace.badResponses.filter((item) => !allow(item)); + const requestFailures = trace.requestFailures.filter((item) => { + // Chromium reports these when a page is reloaded, closed, or an iframe/game switches targets. + // They are not the same as a server/client failure such as 404, 502, CORS, DNS, or TLS. + if (/net::ERR_ABORTED/i.test(item.failure || '')) return false; + return !allow(item); + }); + const consoleMessages = trace.consoleMessages.filter((item) => !/favicon|sandbox/i.test(item.text) && !allow(item)); + return { + clean: badResponses.length === 0 && requestFailures.length === 0 && trace.pageErrors.length === 0 && consoleMessages.length === 0, + badResponses, + requestFailures, + pageErrors: trace.pageErrors, + consoleMessages, + }; +} + +async function screenshot(page, name) { + const file = path.join(proofDir, `${name}.png`); + await page.screenshot({ path: file, fullPage: true }); + return file; +} + +async function saveJson(name, payload) { + const file = path.join(proofDir, `${name}.json`); + fs.writeFileSync(file, JSON.stringify(payload, null, 2)); + return file; +} + +async function testHealth() { + const urls = [ + `${base}/api/health`, + 'https://brain.daveai.tech/health', + 'https://voice.daveai.tech/health', + 'https://api.daveai.tech/health', + 'https://iptv.daveai.tech/api/provider-vault/providers', + ]; + const checks = []; + for (const url of urls) { + const response = await fetch(url, { redirect: 'manual' }); + checks.push({ url, status: response.status, contentType: response.headers.get('content-type'), body: (await response.text()).slice(0, 300) }); + } + await saveJson('health-endpoints', checks); + const failed = checks.filter((check) => check.status !== 200); + record('health endpoints', failed.length ? 'failed' : 'passed', { checks, failed }); +} + +async function testUnauthChatGate(browser) { + const page = await newTrackedPage(browser, 'chat-auth-gate'); + await page.goto(`${base}/?intro=off&e2e=auth-gate-${Date.now()}`, { waitUntil: 'domcontentloaded', timeout: 45000 }); + await page.evaluate(() => { + localStorage.removeItem('daveai_token'); + localStorage.removeItem('daveai_token_ts'); + }); + await page.reload({ waitUntil: 'domcontentloaded' }); + await page.waitForTimeout(3000); + const state = await page.evaluate(() => ({ + authVisible: Boolean(document.querySelector('#auth-modal, .auth-modal, [data-auth-modal]')) || /sign in|create one|forgot password/i.test(document.body.innerText), + typing: /thinking/i.test(document.body.innerText), + bodyText: document.body.innerText.slice(0, 1000), + })); + const image = await screenshot(page, 'chat-auth-gate'); + const trace = assertCleanTrace(page._daveTrace, (item) => /\/api\/health/.test(item.url || '')); + await saveJson('chat-auth-gate', { state, trace, image }); + await page.close(); + record('chat auth gate', state.authVisible && trace.clean ? 'passed' : 'failed', { state, trace, image }); +} + +async function testAuthenticatedChat(browser) { + if (!authEmail || !authPassword) { + record('authenticated chat reply', 'skipped', { reason: 'Set DAVEAI_E2E_EMAIL and DAVEAI_E2E_PASSWORD to run this proof.' }); + return; + } + const page = await newTrackedPage(browser, 'authenticated-chat'); + await page.goto(`${base}/?intro=off&e2e=chat-${Date.now()}`, { waitUntil: 'domcontentloaded', timeout: 45000 }); + await page.fill('input[type="email"], input[name="email"]', authEmail); + await page.fill('input[type="password"], input[name="password"]', authPassword); + await page.getByRole('button', { name: /sign in|log in|login/i }).first().click(); + await page.waitForTimeout(3000); + const input = page.locator('#mc-input, textarea[placeholder*="Describe"], input[placeholder*="Describe"]').first(); + await input.fill('Say exactly: production chat e2e ok'); + await page.keyboard.press('Enter'); + await page.waitForFunction(() => /production chat e2e ok|Ready/i.test(document.body.innerText), null, { timeout: 90000 }).catch(() => {}); + await page.waitForTimeout(2000); + const state = await page.evaluate(() => ({ bodyText: document.body.innerText.slice(-2500), ready: /Ready/i.test(document.body.innerText) })); + const image = await screenshot(page, 'authenticated-chat'); + const trace = assertCleanTrace(page._daveTrace); + await saveJson('authenticated-chat', { state, trace, image }); + await page.close(); + record('authenticated chat reply', state.ready && trace.clean ? 'passed' : 'failed', { state, trace, image }); +} + +async function testCarousel(browser) { + const page = await newTrackedPage(browser, 'carousel-launch'); + await page.goto(`${base}/?intro=off&e2e=carousel-${Date.now()}`, { waitUntil: 'domcontentloaded', timeout: 45000 }); + await page.waitForTimeout(4000); + const launches = []; + for (let index = 0; index < 3; index += 1) { + await page.evaluate((i) => { + if (typeof window.launchCarouselGame === 'function') window.launchCarouselGame(i); + else document.querySelectorAll('[data-game-action="launch"], .game-card')[i]?.click(); + }, index); + await page.waitForTimeout(3500); + launches.push(await page.evaluate(() => { + const iframe = document.querySelector('iframe#game-frame, iframe[src*="/games/"]'); + return { iframeSrc: iframe?.src || '', bodyText: document.body.innerText.slice(0, 800) }; + })); + await screenshot(page, `carousel-launch-${index}`); + } + const trace = assertCleanTrace(page._daveTrace); + await saveJson('carousel-launch', { launches, trace }); + await page.close(); + record('carousel launch', launches.every((item) => /\/games\//.test(item.iframeSrc)) && trace.clean ? 'passed' : 'failed', { launches, trace }); +} + +async function testCoreGames(browser) { + const games = [ + { id: 'td2', url: `${base}/games/daveai-td2/index.html`, expect: /DaveAI TD2/i }, + { id: 'asteroids', url: `${base}/games/asteroids/index.html`, expect: /Asteroids/i }, + { id: 'siege', url: `${base}/games/daves-siege-td/index.html`, expect: /Siege TD|Dave's Siege/i }, + ]; + const gameResults = []; + for (const game of games) { + const page = await newTrackedPage(browser, `game-${game.id}`); + await page.goto(`${game.url}?e2e=${Date.now()}`, { waitUntil: 'domcontentloaded', timeout: 45000 }); + await page.waitForTimeout(9000); + const state = await page.evaluate(() => ({ + title: document.title, + h1: document.querySelector('h1')?.innerText || '', + canvasCount: document.querySelectorAll('canvas').length, + buttonText: [...document.querySelectorAll('button')].map((button) => button.innerText || button.getAttribute('aria-label') || '').filter(Boolean).slice(0, 20), + bodyText: document.body.innerText.slice(0, 1200), + })); + const image = await screenshot(page, `game-${game.id}`); + const trace = assertCleanTrace(page._daveTrace, (item) => /WebGL|AudioContext|Autoplay/i.test(item.text || '')); + const passed = trace.clean && (state.canvasCount > 0 || game.expect.test(`${state.title}\n${state.h1}\n${state.bodyText}`)); + gameResults.push({ game, state, trace, image, passed }); + await page.close(); + } + await saveJson('core-games', gameResults); + record('TD2/Asteroids/Siege runtime checks', gameResults.every((result) => result.passed) ? 'passed' : 'failed', { gameResults }); +} + +async function testNonGamePages(browser) { + const pages = [ + { id: 'studio', url: `${base}/studio/index.html` }, + { id: 'web-pages', url: `${base}/web-pages.html` }, + { id: 'windsurf', url: `${base}/windsurf.html` }, + { id: 'voice', url: 'https://voice.daveai.tech/' }, + { id: 'voice-landing', url: `${base}/voice-landing.html` }, + { id: 'streamhub', url: 'https://iptv.daveai.tech/' }, + ]; + const pageResults = []; + for (const item of pages) { + const page = await newTrackedPage(browser, `page-${item.id}`); + await page.goto(`${item.url}${item.url.includes('?') ? '&' : '?'}e2e=${Date.now()}`, { waitUntil: 'domcontentloaded', timeout: 45000 }); + await page.waitForTimeout(5000); + const state = await page.evaluate(() => ({ + title: document.title, + h1: document.querySelector('h1')?.innerText || '', + authGate: Boolean(document.querySelector('#windsurf-auth-gate')), + videoCount: document.querySelectorAll('video').length, + bodyText: document.body.innerText.slice(0, 1000), + })); + const image = await screenshot(page, `page-${item.id}`); + const trace = assertCleanTrace(page._daveTrace); + pageResults.push({ item, state, trace, image, passed: trace.clean }); + await page.close(); + } + await saveJson('non-game-pages', pageResults); + record('non-game pages', pageResults.every((result) => result.passed) ? 'passed' : 'failed', { pageResults }); + const streamhub = pageResults.find((result) => result.item.id === 'streamhub'); + record('StreamHub no-error render', streamhub && streamhub.passed && streamhub.state.videoCount > 0 ? 'passed' : 'failed', { streamhub }); +} + +(async () => { + const browser = await chromium.launch({ headless: true }); + try { + await testHealth(); + await testUnauthChatGate(browser); + await testAuthenticatedChat(browser); + await testCarousel(browser); + await testCoreGames(browser); + await testNonGamePages(browser); + } finally { + await browser.close(); + } + + const summary = { + generatedAt: new Date().toISOString(), + base, + proofDir, + results, + passed: results.filter((result) => result.status === 'passed').length, + failed: results.filter((result) => result.status === 'failed').length, + skipped: results.filter((result) => result.status === 'skipped').length, + }; + await saveJson('summary', summary); + console.log(JSON.stringify(summary, null, 2)); + process.exit(summary.failed === 0 ? 0 : 2); +})().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/vps/proofs/DAVEAI_E2E_COMPLETION_20260724.md b/vps/proofs/DAVEAI_E2E_COMPLETION_20260724.md new file mode 100644 index 00000000000..87b2dd2c15e --- /dev/null +++ b/vps/proofs/DAVEAI_E2E_COMPLETION_20260724.md @@ -0,0 +1,254 @@ +# DaveAI End-to-End Completion Report + +- Date: 2026-07-25 +- Production: https://daveai.tech/ +- Brain: healthy, version 4.0.0 +- Canonical UI source SHA-256: `a9dc0ebfb1e6e87f62329e642892e674a573bdddd70f8a0d5fd3408b826e3cff` +- Canonical CSS source SHA-256: `bca841c9633c7ef3420865fbdf41872ef4cd4130f71004842aa084b4f2246392` +- Last verified production UI SHA-256: `a9dc0ebfb1e6e87f62329e642892e674a573bdddd70f8a0d5fd3408b826e3cff` +- Last verified production CSS SHA-256: `bca841c9633c7ef3420865fbdf41872ef4cd4130f71004842aa084b4f2246392` +- Accessibility deployment backup: `/opt/daveai/backups/20260725T104457Z-accessibility-contract` + +## Outcome + +The DaveAI website shell, project-building workflow, voice workflow, files +workspace, agent routing, approval gates, project isolation, and production +preview path have completed their final end-to-end pass. No known website-level +wiring defect remains in the audited surfaces. + +The root page and V6 page contain the same deployed HTML. The Brain reports 120 +live runtime tools and four agents: Supervisor, Coder, Asset, and QA. + +The production UI is byte-identical to both canonical repository copies. The +accessibility follow-up below is deployed with matching production hashes and +the `20260725-accessibility-1` stylesheet release key. + +A source-of-truth verifier blocks deployment when the UI copies, the responsive +stylesheet contract, Sites truth, critical project statuses, mode/workspace +state rules, or status-label contrast drift. + +## Responsive Workspace Completion + +The structural workspace-preset follow-up is deployed and verified in the live +site. All 12 presets were exercised through the production layout picker in +both Mobile and Tablet modes. + +- Tablet retained its 768-pixel action window across every preset. +- Mobile retained its 375-pixel action window across every preset. +- Compact, expanded, minimal, top-docked, bottom-docked, rail-free, wide-rail, + left-Activity, right-Activity, and Activity-free variants changed actual + workspace geometry rather than only colour. +- Chronicle opened a 420-pixel Activity drawer on the left; Horizons opened a + 360-pixel Activity drawer on the right. +- Every preset kept the preview and composer contained with no root, chat, + composer, or top-bar horizontal overflow. +- The four agent controls retained transparent backgrounds and readable agent + colours without hover. +- The final browser console contained no errors or warnings. + +## Repository Accessibility Follow-up + +The repository follow-up on 2026-07-25 completed the legacy click-target and +dynamic-ID audit. + +- The Git checkout was safely reconnected to `Ghenghis/bolt.diy` as `origin` + and `stackblitz-labs/bolt.diy` as `upstream`, both at `main` commit + `2e254ac19a696394030601bc602f54945b12bfc4`. +- All clickable `div` and `span` controls now expose an interaction role, + keyboard focus, and Enter/Space activation, except modal backdrops whose + click handler is only an optional outside-dismiss affordance. +- The 22 static service destinations are native links with safe new-tab + attributes. +- The stylesheet release key is `20260725-accessibility-1`, ensuring the + accessibility CSS is fetched instead of an older cached asset. +- `fp-tool-count` is now a real live-status node in the Tools panel. +- Seven referenced IDs are confirmed runtime-created: + `admin-vps-dash`, `cmd-palette-overlay`, `demo-restore-btn`, `profile-box`, + `profile-ov`, `shortcuts-ov`, and `typing-ind`. +- The accessibility contract now fails for unresolved ID references or + non-keyboard click targets. Canonical and runtime-mirror scans both pass with + zero unresolved IDs, zero unnamed buttons, and zero non-keyboard targets. +- Production keyboard verification passed for the Tools tab, prompt + suggestion, preview device switcher, game carousel, and Voice Studio Presets + tab. Desktop mode was restored, Voice Studio was closed, and the final + browser console contained no errors or warnings. + +## Final Browser Regression + +| Surface | Final production result | +|---|---| +| Left flyouts | Projects, Pages, Sites, Tools, Agents, Skills, and Database all opened the correct visible panel | +| Fixed left actions | Deploy, Analytics, and Settings opened the correct full workspace | +| Action modes | Chat, Browser, Build, Code, Files, Research, Solve, Game, Hermes, and Admin/Ops all changed the center action surface | +| Right panel | Activity, Memory, and History selected correctly | +| Mode/right-panel behavior | Memory remained selected through all ten action-mode changes | +| Agent selectors | Supervisor, Coder, QA, and Asset each became the single pressed/selected agent | +| Responsive modes | Desktop, Tablet, Android, and Mobile each changed the preview class; Desktop was restored | +| Canvas controls | Code opened the live Files pane; Preview restored the action window | +| Canvas/mode state | Code no longer overwrites the selected global mode; Preview restores the selected mode and composer action state | +| Locked mode semantics | Admin/Ops remains locked for non-admin users and exposes `aria-disabled="true"` with an admin-role explanation | +| Voice controls | Text, Mic-to-Text, Text-to-Voice, and Full Voice changed visible state correctly; the mode accessibility label now changes with the visible state | +| Voice picker | 99 Azure English Neural voices, one selected voice, open/close passed | +| Voice toggles | Mute, Auto-read, and Think Aloud changed and restored state | +| Voice Studio | Open/close passed; Azure diagnostics reported 4/4 | +| Workspace layouts | 12 cards, one selected layout, apply/close passed | +| Pages action | New Page prepared an editable builder prompt | +| Sites action | New Site prepared an editable builder prompt | +| Database action | `SELECT 1 AS ok` returned one row with `ok: 1` | +| Skills action | Web Builder prepared the matching editable prompt | +| Projects action | DaveAI Website loaded into the preview and closed the flyout | + +## Runtime Tool Truth + +The previous UI catalog contained 113 hand-authored entries and did not match +the actual runtime. The Tools panel now treats `/api/tools` as authoritative. + +- 120 visible tool rows +- 120 unique tool names +- 120 Run buttons +- Missing runtime entries such as `accessibility_check` and + `zeroclaw_workspace_tree` now appear +- Stale UI-only entries such as `a11y_check` are removed +- The left label, search placeholder, Agents panel, Admin panel, and status bar + all report the same live count +- Opening `accessibility_check` produced the correct metadata and Run prepared + the capability-broker request in the composer +- Destructive names continue to prepare approval-gated requests and do not + execute without approval + +## Route and Status Truth + +The final route sweep matched every Sites label after the nine remaining +services were completed: + +| Label | Count | HTTP proof | +|---|---:|---| +| live | 14 | 14 public routes returned HTTP 200 | +| auth | 8 | 8 protected roots returned the Authelia redirect and their public health checks passed | +| soon | 0 | No Sites entry remains labeled `soon` | + +Projects now uses the same hostname truth. DaveAI Dev, Staging, and DIY display +`auth`; Hermes3D displays `live`; the protected game portal remains `auth`. The game carousel +contains only three unique live projects and no protected or duplicate card. +The Sites panel renders all 22 catalog rows with readable, distinct live and +auth badges. + +## Nine-Service Completion + +| Service | Accepted production behavior | Evidence | +|---|---|---| +| DIY Studio | Validated multi-file project scaffolds behind Authelia | `ev_eb85b65e687198da` | +| Developer Console | 4/4 real dependency probes healthy behind Authelia | `ev_8a8671df8e0232de` | +| Staging Gate | Route checks and canonical deployed UI hash passed | `ev_6953ab2794c3efd6` | +| Gitea | Persistent, loopback-only, install-locked repository service | `ev_601951b01a1772fc` | +| Adminer | Private Bolt PostgreSQL network and TCP connectivity proven | `ev_7965946348120e57` | +| Monitoring | Grafana, Prometheus, node-exporter, and six-panel dashboard | `ev_202f2e04efb4e960`, corrected by `ev_99014310b2a74ed0` | +| Fleet | Live redacted PM2/Docker inventory with corrected uptime | `ev_207fb5483a36db91` | +| WebSocket | External WSS TLS upgrade and matching echo | `ev_0bf6cfcd1478c0f3` | +| Hermes3D | Brain 4.0.0, 120 tools, 4/4 agents, browser rotation proof | `ev_1b374693551fbcd1` | + +## Voice-to-Project Golden Path + +Azure transcription produced this request: + +> Create a project called Voice Multi-fileproof. Use index HTML styles, CSS and +> app JS. Display Voice project OK add a styled card and a button that changes +> the status text. Validated and commit it. + +The authenticated Coder stream then: + +1. Created the owner-scoped project workspace. +2. Materialized `index.html`, `styles.css`, and `app.js`. +3. Ran validation and deterministic repair. +4. Loaded the owner-scoped production preview. +5. Clicked the requested control and proved the text changed from + `Voice Project OK` to `Status Changed`. +6. Committed only after the completion gate passed. +7. Finished with a clean Git workspace. + +Project URL: +https://daveai.tech/user-projects/user-16/18-voice-multi-file-proof/index.html + +Commit: `0f7e74b93c89d14812d74739d32bcd22e5bb5dd1` + +Hashes: + +- `index.html`: `1eb6803c8b9d3826318e27d4522dd3f36bac66ca0186db96d5cd459fa44781ac` +- `styles.css`: `f52fe86d74ba7234fbaece27c033b1fc54ff22cad37ca6051accf1a3a07220f4` +- `app.js`: `6ffce921859e058b062f0495bbb5a0db29101f1b49dd29e9b7465bbd35d61c27` + +## Files Workspace + +The production Files mode passed: + +- authenticated owner-scoped tree load +- file open and editable contents +- save +- new file +- rename +- preview refresh +- project selection persistence + +Path overrides remain server-authorized; browser callers cannot escape the +authenticated user's project workspace. + +## Security Completion + +- Project and stream endpoints require DaveAI authentication. +- Normal users receive owner-scoped projects and files. +- Cross-owner project access is denied. +- Admin-only runtime and infrastructure operations remain admin-gated. +- Destructive tool requests retain explicit approval gates. +- Azure speech synthesis now requires an authenticated DaveAI user. +- Anonymous Azure synthesis returned HTTP 401. +- The browser sends its DaveAI authorization header for Voice Studio diagnostics + and speech synthesis. + +## Proof Artifacts + +- `20260724-mode-memory-wiring.png` +- `20260724-tools-runtime-panel.png` +- `20260724-sites-truth-panel.png` +- `20260724-canonical-catalog-truth.png` +- `20260724-canonical-catalog-truth-bottom.png` +- `20260725-hermes3d-live-agent-graph.png` +- `20260724-files-editor.png` +- `20260724-voice-project.png` +- `20260724-voice-build-prompt.mp3` + +Hash-chained evidence ledger entries: + +- Production release: `ev_5f3fa08b389e06ca` +- Final browser E2E: `ev_1a4c71a073d55555` +- Voice project E2E: `ev_50d57a6956031f79` +- Canonical source/catalog and final regression: `ev_fa00a46449d12a3c` + +## Production Rollback + +Latest release rollback: + +`/opt/daveai/backups/20260725T024344Z-canonical-state-contrast` + +Additional focused rollback points: + +- `/opt/daveai/backups/20260725T023935Z-canvas-mode-state` +- `/opt/daveai/backups/20260724T220259Z-canonical-source-catalogs` +- `/opt/daveai/backups/20260724T200634Z-carousel-project-truth` +- `/opt/daveai/backups/20260724T200502Z-runtime-tools-project-truth` +- `/opt/daveai/backups/20260724T195825Z-final-ui-truth-tts-auth` + +## Operational Boundaries + +The main DaveAI website and all nine formerly `soon` subdomain services are +complete for the audited workflow. No Sites entry remains labeled `soon`. +Protected service roots remain labeled `auth` rather than being falsely +represented as public. + +Tool Run controls prepare reviewable agent/broker requests. A tool executes +after the user sends that request, and destructive operations still require +approval. The audit did not execute all 120 tools destructively because doing so +would alter or remove live infrastructure. + +DaveAI can create and validate complete multi-file projects. Output quality and +speed still depend on the configured model provider; the local fallback model +remains available when cloud routing is unavailable. diff --git a/vps/verify-daveai-ui-contracts.cjs b/vps/verify-daveai-ui-contracts.cjs new file mode 100644 index 00000000000..80b1060fd12 --- /dev/null +++ b/vps/verify-daveai-ui-contracts.cjs @@ -0,0 +1,120 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('node:fs'); + +const htmlPath = process.argv[2]; +if (!htmlPath) { + throw new Error('usage: node verify-daveai-ui-contracts.cjs '); +} +const html = fs.readFileSync(htmlPath, 'utf8'); + +const ids = [...html.matchAll(/\bid="([^"]+)"/g)].map(match => match[1]); +const idCounts = new Map(); +for (const id of ids) idCounts.set(id, (idCounts.get(id) || 0) + 1); +const duplicateIds = [...idCounts].filter(([, count]) => count > 1); + +const declaredFunctions = new Set([ + ...[...html.matchAll(/\b(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/g)].map(match => match[1]), + ...[...html.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/g)].map(match => match[1]), + ...[...html.matchAll(/\bwindow\.([A-Za-z_$][\w$]*)\s*=/g)].map(match => match[1]), +]); + +const handlerAttributes = [...html.matchAll(/\bon(?:click|change|input|keydown|keyup|contextmenu|mousedown|mouseup)="([^"]*)"/gi)]; +const ignoredCalls = new Set([ + 'alert', 'confirm', 'prompt', 'fetch', 'open', 'setTimeout', 'clearTimeout', + 'parseInt', 'parseFloat', 'String', 'Number', 'Boolean', 'Date', 'Array', + 'Object', 'JSON', 'Math', 'encodeURIComponent', 'decodeURIComponent', +]); +const handlerCalls = new Set(); +for (const match of handlerAttributes) { + for (const call of match[1].matchAll(/(? !declaredFunctions.has(name)).sort(); + +const referencedIds = new Set([ + ...[...html.matchAll(/getElementById\(\s*['"]([^'"]+)['"]\s*\)/g)].map(match => match[1]), + ...[...html.matchAll(/querySelector\(\s*['"]#([A-Za-z][\w:-]*)['"]\s*\)/g)].map(match => match[1]), +]); +const createdIds = new Set([ + ...[...html.matchAll(/\.id\s*=\s*['"]([^'"]+)['"]/g)].map(match => match[1]), + ...[...html.matchAll(/setAttribute\(\s*['"]id['"]\s*,\s*['"]([^'"]+)['"]\s*\)/g)].map(match => match[1]), +]); +const referencedIdsWithoutStaticNodes = [...referencedIds].filter(id => !idCounts.has(id)); +const dynamicallyCreatedIds = referencedIdsWithoutStaticNodes.filter(id => createdIds.has(id)).sort(); +const missingStaticIds = referencedIdsWithoutStaticNodes.filter(id => !createdIds.has(id)).sort(); + +const unnamedButtons = []; +for (const match of html.matchAll(/]*)>([\s\S]*?)<\/button>/gi)) { + const attrs = match[1]; + const body = match[2]; + const text = body + .replace(/<[^>]+>/g, ' ') + .replace(/&[A-Za-z0-9#]+;/g, ' ') + .replace(/\$\{[^}]+\}/g, ' dynamic ') + .replace(/\s+/g, ' ') + .trim(); + const named = /\b(?:aria-label|title)="[^"]+"/i.test(attrs) || text.length > 0; + if (!named) { + const line = html.slice(0, match.index).split('\n').length; + unnamedButtons.push({ line, attrs: attrs.trim().replace(/\s+/g, ' ').slice(0, 160) }); + } +} + +const nonKeyboardClickTargets = []; +for (const match of html.matchAll(/<(div|span)\b([^>]*\bonclick="[^"]+"[^>]*)>/gi)) { + const attrs = match[2]; + const role = attrs.match(/\brole="([^"]+)"/i)?.[1] || ''; + const hasTabIndex = /\btabindex="-?\d+"/i.test(attrs); + const hasKeyboardHandler = /\bonkey(?:down|up)="[^"]+"/i.test(attrs); + const isBackdropDismissal = role === 'dialog' + && /event\.target(?:\.id)?\s*={2,3}\s*(?:this|['"][^'"]+['"])/i.test(attrs); + if (!isBackdropDismissal && (!role || !hasTabIndex || !hasKeyboardHandler)) { + const line = html.slice(0, match.index).split('\n').length; + nonKeyboardClickTargets.push({ + line, + tag: match[1], + missing: [ + !role ? 'role' : null, + !hasTabIndex ? 'tabindex' : null, + !hasKeyboardHandler ? 'keyboard-handler' : null, + ].filter(Boolean), + attrs: attrs.trim().replace(/\s+/g, ' ').slice(0, 160), + }); + } +} + +const scriptBlocks = [...html.matchAll(/]*)?>([\s\S]*?)<\/script>/gi)].map(match => match[1]); +for (const [index, block] of scriptBlocks.entries()) { + try { + new Function(block); + } catch (error) { + throw new Error(`inline script ${index} failed to compile: ${error.message}`); + } +} + +const result = { + inlineScriptsCompiled: scriptBlocks.length, + staticIds: ids.length, + duplicateIds, + handlerAttributes: handlerAttributes.length, + missingHandlerFunctions, + referencedStaticIds: referencedIds.size, + dynamicallyCreatedIds, + missingStaticIds, + unnamedButtons, + nonKeyboardClickTargets, +}; + +console.log(JSON.stringify(result, null, 2)); +if ( + duplicateIds.length + || missingHandlerFunctions.length + || missingStaticIds.length + || unnamedButtons.length + || nonKeyboardClickTargets.length +) { + process.exitCode = 1; +} diff --git a/vps/verify-feature-completion-state-actions.cjs b/vps/verify-feature-completion-state-actions.cjs new file mode 100644 index 00000000000..caf9e4d7674 --- /dev/null +++ b/vps/verify-feature-completion-state-actions.cjs @@ -0,0 +1,128 @@ +#!/usr/bin/env node +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const vm = require('node:vm'); + +const htmlPath = process.argv[2]; +if (!htmlPath) { + throw new Error('usage: node verify-feature-completion-state-actions.js '); +} +const html = fs.readFileSync(htmlPath, 'utf8'); + +function sourceBetween(startMarker, endMarker) { + const start = html.indexOf(startMarker); + const end = html.indexOf(endMarker, start + startMarker.length); + assert.ok(start >= 0, `missing source marker: ${startMarker}`); + assert.ok(end > start, `missing source marker: ${endMarker}`); + return html.slice(start, end); +} + +function storage(initial = {}) { + const values = new Map(Object.entries(initial)); + return { + getItem: key => values.has(key) ? values.get(key) : null, + setItem: (key, value) => values.set(key, String(value)), + removeItem: key => values.delete(key), + }; +} + +const memoryKey = 'daveai_memory'; +const backupKey = 'daveai_memory_backup'; +const undoButton = { disabled: true }; +const memoryEvents = []; +const memoryContext = vm.createContext({ + console, + JSON, + Date, + confirm: () => true, + localStorage: storage({ [memoryKey]: '{"facts":["proof"]}' }), + document: { getElementById: id => id === 'stg-memory-undo' ? undoButton : null }, + addRichActivity: event => memoryEvents.push(event), + _MEMORY_KEY: memoryKey, + _MEMORY_BACKUP_KEY: backupKey, +}); +vm.runInContext( + sourceBetween('function _clearMemory()', 'function _exportMemory()'), + memoryContext, +); +memoryContext._clearMemory(); +assert.equal(memoryContext.localStorage.getItem(memoryKey), null); +assert.ok(memoryContext.localStorage.getItem(backupKey)); +assert.equal(undoButton.disabled, false); +memoryContext._undoClearMemory(); +assert.equal(memoryContext.localStorage.getItem(memoryKey), '{"facts":["proof"]}'); +assert.equal(memoryContext.localStorage.getItem(backupKey), null); +assert.equal(undoButton.disabled, true); +assert.match(memoryEvents.at(-1).msg, /restored/i); + +let presetSaves = 0; +let presetRenders = 0; +const presetEvents = []; +const presetContext = vm.createContext({ + console, + confirm: () => true, + _vsState: { userPresets: [{ name: 'Disposable proof preset' }] }, + vsSave: () => { presetSaves += 1; }, + _vsRenderPresets: () => { presetRenders += 1; }, + addRichActivity: event => presetEvents.push(event), +}); +vm.runInContext( + sourceBetween('function vsDeleteUserPreset(idx)', '// ── Save / Export / Import / Share'), + presetContext, +); +presetContext.vsDeleteUserPreset(0); +assert.equal(presetContext._vsState.userPresets.length, 0); +assert.equal(presetSaves, 1); +assert.equal(presetRenders, 1); +assert.match(presetEvents.at(-1).msg, /Deleted voice preset/); + +let resetSaves = 0; +let resetPopulates = 0; +const resetEvents = []; +const resetContext = vm.createContext({ + console, + confirm: () => true, + _vsState: { voice: 'mutated' }, + _vsDefaults: () => ({ voice: 'default-proof' }), + vsSave: () => { resetSaves += 1; }, + vsPopulateAll: () => { resetPopulates += 1; }, + addRichActivity: event => resetEvents.push(event), +}); +vm.runInContext( + sourceBetween('function vsResetDefaults()', '// ── Moshi Instant Voice'), + resetContext, +); +resetContext.vsResetDefaults(); +assert.equal(resetContext._vsState.voice, 'default-proof'); +assert.equal(resetSaves, 1); +assert.equal(resetPopulates, 1); +assert.match(resetEvents.at(-1).msg, /reset to defaults/); + +let tokenClears = 0; +let currentUser = { email: 'proof@example.test' }; +const authOverlay = { + classList: { remove: name => assert.equal(name, 'hid') }, + style: {}, +}; +const authStorage = storage({ daveai_user: JSON.stringify(currentUser) }); +const signOutContext = vm.createContext({ + console, + clearToken: () => { tokenClears += 1; }, + localStorage: authStorage, + _setCurrentUser: value => { currentUser = value; }, + document: { getElementById: id => id === 'am-ov' ? authOverlay : null }, +}); +vm.runInContext( + sourceBetween('function showAuth()', '// ══ WORKSPACE LAYOUTS'), + signOutContext, +); +signOutContext.showAuth(); +assert.equal(tokenClears, 1); +assert.equal(authStorage.getItem('daveai_user'), null); +assert.equal(currentUser, null); +assert.equal(authOverlay.style.opacity, 1); +assert.equal(authOverlay.style.pointerEvents, ''); + +console.log('memory_clear=pass memory_undo=pass preset_delete=pass voice_reset=pass sign_out=pass'); diff --git a/vps/verify-production-route-truth.cjs b/vps/verify-production-route-truth.cjs new file mode 100644 index 00000000000..b95e622fdb0 --- /dev/null +++ b/vps/verify-production-route-truth.cjs @@ -0,0 +1,103 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); + +const root = __dirname; +const sitesPath = path.join(root, 'daveai-sites-config.json'); +const outputFlag = process.argv.indexOf('--output'); +const outputPath = outputFlag >= 0 ? process.argv[outputFlag + 1] : ''; +const document = JSON.parse(fs.readFileSync(sitesPath, 'utf8')); +const sites = document.sites || []; + +function sha256(file) { + return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); +} + +async function check(site) { + const before = Date.now(); + try { + const response = await fetch(site.url, { + redirect: 'manual', + headers: { + 'Cache-Control': 'no-cache', + 'User-Agent': 'DaveAI-Route-Truth/1.0', + }, + signal: AbortSignal.timeout(12000), + }); + const location = response.headers.get('location') || ''; + let finalHttpStatus = response.status; + let finalUrl = response.url; + if (site.status === 'live' && [301, 302, 303, 307, 308].includes(response.status)) { + const followed = await fetch(site.url, { + redirect: 'follow', + headers: { + 'Cache-Control': 'no-cache', + 'User-Agent': 'DaveAI-Route-Truth/1.0', + }, + signal: AbortSignal.timeout(12000), + }); + finalHttpStatus = followed.status; + finalUrl = followed.url; + } + const livePassed = site.status === 'live' && finalHttpStatus === 200; + const authPassed = site.status === 'auth' + && [302, 303, 307, 308].includes(response.status) + && location.startsWith('https://auth.daveai.tech/'); + return { + domain: site.domain, + label: site.status, + url: site.url, + httpStatus: response.status, + location: location || null, + finalHttpStatus, + finalUrl, + latencyMs: Date.now() - before, + passed: livePassed || authPassed, + }; + } catch (error) { + return { + domain: site.domain, + label: site.status, + url: site.url, + httpStatus: 0, + location: null, + latencyMs: Date.now() - before, + passed: false, + error: error.name === 'TimeoutError' ? 'timeout' : error.message, + }; + } +} + +async function main() { + if (sites.length !== 22) throw new Error(`Expected 22 sites, found ${sites.length}`); + if (sites.some((site) => site.status === 'soon')) throw new Error('A soon route remains in the source catalog'); + const results = await Promise.all(sites.map(check)); + const counts = results.reduce((summary, result) => { + summary[result.label] = (summary[result.label] || 0) + 1; + return summary; + }, {}); + const failed = results.filter((result) => !result.passed); + const proof = { + schema: 'daveai-route-truth-proof/v1', + checkedAt: new Date().toISOString(), + sitesConfigSha256: sha256(sitesPath), + counts, + passed: failed.length === 0 && counts.live === 14 && counts.auth === 8, + failed: failed.map((result) => result.domain), + results, + }; + const serialized = `${JSON.stringify(proof, null, 2)}\n`; + if (outputPath) { + fs.mkdirSync(path.dirname(path.resolve(outputPath)), { recursive: true }); + fs.writeFileSync(outputPath, serialized); + } + process.stdout.write(serialized); + if (!proof.passed) process.exitCode = 1; +} + +main().catch((error) => { + process.stderr.write(`${error.stack}\n`); + process.exitCode = 1; +}); diff --git a/vps/verify-source-of-truth.py b/vps/verify-source-of-truth.py new file mode 100644 index 00000000000..969a4cc53c5 --- /dev/null +++ b/vps/verify-source-of-truth.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Fail fast when DaveAI deploy sources drift from verified production truth.""" + +from __future__ import annotations + +import hashlib +import json +from collections import Counter +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent +UI = ROOT / "daveai-ui-v6.html" +CSS = ROOT / "assets" / "daveai-v6.css" +RUNTIME_UI = ( + ROOT + / "patches" + / "agent-runtime-completion-20260724-src" + / "daveai-ui-v6.html" +) +SITES = ROOT / "daveai-sites-config.json" +PROJECTS = ROOT / "daveai-project-catalog.json" + +EXPECTED_SOON: set[str] = set() +EXPECTED_AUTH = { + "diy.daveai.tech", + "dev.daveai.tech", + "staging.daveai.tech", + "git.daveai.tech", + "db.daveai.tech", + "monitor.daveai.tech", + "fleet.daveai.tech", + "game.daveai.tech", +} +EXPECTED_NEW_HOST_STATUS = { + "diy.daveai.tech": "auth", + "dev.daveai.tech": "auth", + "staging.daveai.tech": "auth", + "git.daveai.tech": "auth", + "db.daveai.tech": "auth", + "monitor.daveai.tech": "auth", + "fleet.daveai.tech": "auth", + "ws.daveai.tech": "live", + "hermes3d.daveai.tech": "live", +} +EXPECTED_PROJECT_STATUS = { + "srv-daveai-dev": "auth", + "srv-daveai-staging": "auth", + "srv-daveai-diy": "auth", + "srv-hermes": "auth", + "srv-hermes3d": "live", + "srv-fleet": "auth", + "srv-auth": "live", + "srv-bolt": "auth", + "srv-hermestv": "live", + "srv-tv": "live", + "srv-games-portal": "auth", + "srv-game-gangster-wars": "auth", +} + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def require(condition: bool, message: str) -> None: + if not condition: + raise SystemExit(f"source-of-truth verification failed: {message}") + + +def main() -> None: + for path in (UI, CSS, RUNTIME_UI, SITES, PROJECTS): + require(path.is_file(), f"missing {path}") + + ui_hash = sha256(UI) + css_hash = sha256(CSS) + runtime_ui_hash = sha256(RUNTIME_UI) + require(ui_hash == runtime_ui_hash, "top-level UI differs from runtime bundle") + + ui_text = UI.read_text(encoding="utf-8") + css_text = CSS.read_text(encoding="utf-8") + require( + '/assets/daveai-v6.css?v=20260725-accessibility-1' in ui_text, + "UI does not reference the canonical responsive workspace stylesheet", + ) + for selector in ( + 'body[data-device-mode="tablet"] #bframe', + 'body.workspace-rp-hidden #rp-tb', + 'body[data-device-mode="tablet"].workspace-chat-expanded #chat', + 'body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-compact #chat', + ): + require(selector in css_text, f"responsive stylesheet contract missing: {selector}") + for stale in ("Tools (113)", "Search 113 tools", "113 catalog / 116 runtime"): + require(stale not in ui_text, f"stale UI count remains: {stale}") + require( + "document.body.setAttribute('data-mode', 'code')" not in ui_text, + "canvas Code tab must not overwrite the selected global mode", + ) + require( + "document.body.setAttribute('data-mode', selectedMode)" in ui_text, + "Preview must restore the selected global mode after leaving Code", + ) + require( + 'aria-disabled="true" title="Admin role required"' in ui_text, + "locked Admin mode must expose disabled semantics", + ) + require( + '#fp-sites .site-badge[style*="#b2bec3"]' in ui_text + and "color:#FCD34D!important" in ui_text, + "soon status badges must remain readable", + ) + require( + '#fp-sites .site-badge[style*="#7C3AED"]' in ui_text + and "color:#EDE9FE!important" in ui_text, + "auth status badge must remain readable", + ) + require( + ">soon" not in ui_text, + "static Sites panel still contains a soon label", + ) + for domain, expected_status in EXPECTED_NEW_HOST_STATUS.items(): + require( + f"'{domain}': '{expected_status}'" in ui_text, + f"project hostname truth missing {domain}={expected_status}", + ) + + sites_doc = json.loads(SITES.read_text(encoding="utf-8")) + sites = sites_doc.get("sites") or [] + require(len(sites) == 22, f"expected 22 sites, found {len(sites)}") + by_domain = {site["domain"]: site for site in sites} + require(len(by_domain) == 22, "duplicate site domains") + counts = Counter(site.get("status") for site in sites) + require(counts == Counter({"live": 14, "auth": 8}), f"bad site counts: {dict(counts)}") + require( + {domain for domain, site in by_domain.items() if site["status"] == "soon"} + == EXPECTED_SOON, + "soon-domain set drifted", + ) + require( + {domain for domain, site in by_domain.items() if site["status"] == "auth"} + == EXPECTED_AUTH, + "auth-domain set drifted", + ) + require( + by_domain["api.daveai.tech"]["url"] == "https://api.daveai.tech/health", + "API site must target its health route", + ) + require( + by_domain["monitor.daveai.tech"]["port"] == 3030, + "monitor site must target the non-conflicting Grafana port", + ) + + project_doc = json.loads(PROJECTS.read_text(encoding="utf-8")) + projects = project_doc.get("projects") or [] + require(len(projects) == 55, f"expected 55 catalog projects, found {len(projects)}") + project_by_id = {project["id"]: project for project in projects} + require(len(project_by_id) == 55, "duplicate project ids") + for project_id, expected_status in EXPECTED_PROJECT_STATUS.items(): + actual = project_by_id.get(project_id, {}).get("status") + require( + actual == expected_status, + f"{project_id} expected {expected_status}, found {actual}", + ) + + print( + json.dumps( + { + "status": "pass", + "ui_sha256": ui_hash, + "css_sha256": css_hash, + "sites": dict(sorted(counts.items())), + "projects": len(projects), + "critical_project_statuses": EXPECTED_PROJECT_STATUS, + }, + indent=2, + sort_keys=True, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/vps/vps-maintenance-check.sh b/vps/vps-maintenance-check.sh new file mode 100644 index 00000000000..4a82b21e151 --- /dev/null +++ b/vps/vps-maintenance-check.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "=== time ===" +date -u + +echo +echo "=== uptime/load ===" +uptime + +echo +echo "=== memory/swap ===" +free -h + +echo +echo "=== disk ===" +df -h / + +echo +echo "=== zombie processes ===" +ps -eo stat,ppid,pid,comm,args | awk '$1 ~ /Z/ {print}' + +echo +echo "=== reboot required ===" +if [ -f /var/run/reboot-required ]; then + cat /var/run/reboot-required + [ -f /var/run/reboot-required.pkgs ] && cat /var/run/reboot-required.pkgs +else + echo "No reboot-required marker found." +fi + +echo +echo "=== pending apt updates count ===" +apt list --upgradable 2>/dev/null | sed '1d' | wc -l + +echo +echo "=== unattended-upgrades recent failures ===" +grep -RIn "ERROR\\|WARNING\\|could not" /var/log/unattended-upgrades 2>/dev/null | tail -40 || true + +echo +echo "=== PM2 status ===" +pm2 status --no-color || true + +echo +echo "=== Docker status ===" +docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' || true + +cat <<'EOF' + +Maintenance-window actions not performed by this diagnostic script: +1. Snapshot/backup VPS. +2. Announce maintenance window. +3. Run apt update && apt upgrade. +4. Reboot if /var/run/reboot-required exists. +5. After reboot, verify: + - nginx -t + - systemctl is-active nginx litellm + - pm2 status + - vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh + - node vps/production-e2e-runner.cjs +EOF