From 95a7781e5af335655052818e48b010728cd9da87 Mon Sep 17 00:00:00 2001 From: Carmel Cohen Date: Tue, 20 Oct 2026 07:18:58 +0000 Subject: [PATCH 01/15] feat: compiled base44-platform skills synced from apper, with validator, auto-merge, CODEOWNERS and docs Adds the `skills/base44-platform-*` tree compiled from the Base44 builder's prompt tree (base44-dev/apper, backend/scripts/agent_skills) at the pinned APPER_COMMIT, the validator that proves a sync PR carries exactly what the compiler emitted, the auto-merge workflow for labeled sync PRs confined to the generated tree, CODEOWNERS for that tree, plugin manifests listing the ten skills, and the README / CONTRIBUTING sections describing the pipeline and how to change a compiled skill (edit the prompt partial in apper). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01YLKGecSgMqR14kYQTrugoR --- .claude-plugin/plugin.json | 28 +- .cursor-plugin/plugin.json | 35 +- .github/CODEOWNERS | 9 + .github/workflows/auto-merge-skill-update.yml | 43 ++ .github/workflows/notify-slack-skill-sync.yml | 5 +- .../workflows/validate-base44-platform.yml | 28 + APPER_COMMIT | 1 + CONTRIBUTING.md | 62 ++ README.md | 14 + .../base44-sandbox/.claude-plugin/plugin.json | 26 +- .../base44-sandbox/.codex-plugin/plugin.json | 32 +- .../base44-sandbox/.cursor-plugin/plugin.json | 32 +- scripts/validate-base44-platform.mjs | 88 +++ skills/base44-platform-actors/SKILL.md | 28 + .../references/mobile.md | 122 ++++ .../base44-platform-actors/references/web.md | 121 ++++ skills/base44-platform-agents/SKILL.md | 23 + .../references/web-spa.md | 196 +++++++ .../references/web-tanstack.md | 185 ++++++ .../SKILL.md | 28 + .../references/mobile.md | 371 ++++++++++++ .../references/web-spa.md | 206 +++++++ .../references/web-tanstack.md | 32 + skills/base44-platform-connectors/SKILL.md | 26 + .../references/mobile.md | 37 ++ .../references/web-spa.md | 34 ++ .../references/web-tanstack.md | 34 ++ skills/base44-platform-entities/SKILL.md | 30 + .../references/mobile-and-games.md | 114 ++++ .../references/rls.md | 144 +++++ .../references/web.md | 55 ++ skills/base44-platform-frontend/SKILL.md | 26 + .../references/mobile.md | 138 +++++ .../references/web-spa.md | 118 ++++ .../references/web-tanstack.md | 212 +++++++ skills/base44-platform-games/SKILL.md | 66 +++ .../references/game-2d.md | 329 +++++++++++ .../references/game-3d.md | 149 +++++ .../phaser/actions_and_utilities.md | 118 ++++ .../references/phaser/animations.md | 249 ++++++++ .../references/phaser/audio_and_sound.md | 229 ++++++++ .../references/phaser/cameras.md | 250 ++++++++ .../references/phaser/curves_and_paths.md | 182 ++++++ .../references/phaser/data_manager.md | 236 ++++++++ .../references/phaser/events_system.md | 193 +++++++ .../references/phaser/filters_and_postfx.md | 198 +++++++ .../phaser/game_object_components.md | 158 +++++ .../phaser/game_setup_and_config.md | 233 ++++++++ .../references/phaser/geometry_and_math.md | 156 +++++ .../references/phaser/graphics_and_shapes.md | 229 ++++++++ .../phaser/groups_and_containers.md | 221 +++++++ .../phaser/input_keyboard_mouse_touch.md | 310 ++++++++++ .../references/phaser/loading_assets.md | 265 +++++++++ .../references/phaser/mesh_and_3d_elements.md | 50 ++ .../references/phaser/particles.md | 290 ++++++++++ .../references/phaser/physics_arcade.md | 256 ++++++++ .../references/phaser/physics_matter.md | 323 +++++++++++ .../references/phaser/render_textures.md | 245 ++++++++ .../references/phaser/scale_and_responsive.md | 188 ++++++ .../references/phaser/scenes.md | 274 +++++++++ .../references/phaser/sprites_and_images.md | 207 +++++++ .../references/phaser/text_and_bitmaptext.md | 249 ++++++++ .../references/phaser/tilemaps.md | 218 +++++++ .../references/phaser/time_and_timers.md | 239 ++++++++ .../references/phaser/tweens.md | 173 ++++++ .../threejs/assets_models_textures.md | 25 + .../references/threejs/cameras.md | 27 + .../threejs/character_controller.md | 25 + .../threejs/enemies_and_movement.md | 31 + .../references/threejs/game_state_and_flow.md | 36 ++ .../references/threejs/input_and_controls.md | 28 + .../references/threejs/performance.md | 33 ++ .../references/threejs/physics_dynamics.md | 26 + .../threejs/projectiles_and_pooling.md | 25 + .../references/threejs/scene_and_rendering.md | 29 + .../references/threejs/text_hud_ui.md | 29 + .../threejs/triggers_and_collisions.md | 28 + .../references/threejs/vfx_and_juice.md | 36 ++ .../base44-platform-integrations-sdk/SKILL.md | 24 + .../references/mobile.md | 185 ++++++ .../references/web.md | 175 ++++++ skills/base44-platform-manifest.json | 82 +++ skills/base44-platform-platform/SKILL.md | 26 + .../references/game.md | 20 + .../references/mobile.md | 58 ++ .../references/web-spa.md | 70 +++ .../references/web-tanstack.md | 70 +++ skills/base44-platform-workflows/SKILL.md | 26 + .../references/automations-legacy.md | 60 ++ .../references/mobile-automations.md | 157 +++++ .../references/web-spa.md | 543 +++++++++++++++++ .../references/web-tanstack.md | 545 ++++++++++++++++++ 92 files changed, 11565 insertions(+), 20 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/auto-merge-skill-update.yml create mode 100644 .github/workflows/validate-base44-platform.yml create mode 100644 APPER_COMMIT create mode 100644 scripts/validate-base44-platform.mjs create mode 100644 skills/base44-platform-actors/SKILL.md create mode 100644 skills/base44-platform-actors/references/mobile.md create mode 100644 skills/base44-platform-actors/references/web.md create mode 100644 skills/base44-platform-agents/SKILL.md create mode 100644 skills/base44-platform-agents/references/web-spa.md create mode 100644 skills/base44-platform-agents/references/web-tanstack.md create mode 100644 skills/base44-platform-backend-functions/SKILL.md create mode 100644 skills/base44-platform-backend-functions/references/mobile.md create mode 100644 skills/base44-platform-backend-functions/references/web-spa.md create mode 100644 skills/base44-platform-backend-functions/references/web-tanstack.md create mode 100644 skills/base44-platform-connectors/SKILL.md create mode 100644 skills/base44-platform-connectors/references/mobile.md create mode 100644 skills/base44-platform-connectors/references/web-spa.md create mode 100644 skills/base44-platform-connectors/references/web-tanstack.md create mode 100644 skills/base44-platform-entities/SKILL.md create mode 100644 skills/base44-platform-entities/references/mobile-and-games.md create mode 100644 skills/base44-platform-entities/references/rls.md create mode 100644 skills/base44-platform-entities/references/web.md create mode 100644 skills/base44-platform-frontend/SKILL.md create mode 100644 skills/base44-platform-frontend/references/mobile.md create mode 100644 skills/base44-platform-frontend/references/web-spa.md create mode 100644 skills/base44-platform-frontend/references/web-tanstack.md create mode 100644 skills/base44-platform-games/SKILL.md create mode 100644 skills/base44-platform-games/references/game-2d.md create mode 100644 skills/base44-platform-games/references/game-3d.md create mode 100644 skills/base44-platform-games/references/phaser/actions_and_utilities.md create mode 100644 skills/base44-platform-games/references/phaser/animations.md create mode 100644 skills/base44-platform-games/references/phaser/audio_and_sound.md create mode 100644 skills/base44-platform-games/references/phaser/cameras.md create mode 100644 skills/base44-platform-games/references/phaser/curves_and_paths.md create mode 100644 skills/base44-platform-games/references/phaser/data_manager.md create mode 100644 skills/base44-platform-games/references/phaser/events_system.md create mode 100644 skills/base44-platform-games/references/phaser/filters_and_postfx.md create mode 100644 skills/base44-platform-games/references/phaser/game_object_components.md create mode 100644 skills/base44-platform-games/references/phaser/game_setup_and_config.md create mode 100644 skills/base44-platform-games/references/phaser/geometry_and_math.md create mode 100644 skills/base44-platform-games/references/phaser/graphics_and_shapes.md create mode 100644 skills/base44-platform-games/references/phaser/groups_and_containers.md create mode 100644 skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md create mode 100644 skills/base44-platform-games/references/phaser/loading_assets.md create mode 100644 skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md create mode 100644 skills/base44-platform-games/references/phaser/particles.md create mode 100644 skills/base44-platform-games/references/phaser/physics_arcade.md create mode 100644 skills/base44-platform-games/references/phaser/physics_matter.md create mode 100644 skills/base44-platform-games/references/phaser/render_textures.md create mode 100644 skills/base44-platform-games/references/phaser/scale_and_responsive.md create mode 100644 skills/base44-platform-games/references/phaser/scenes.md create mode 100644 skills/base44-platform-games/references/phaser/sprites_and_images.md create mode 100644 skills/base44-platform-games/references/phaser/text_and_bitmaptext.md create mode 100644 skills/base44-platform-games/references/phaser/tilemaps.md create mode 100644 skills/base44-platform-games/references/phaser/time_and_timers.md create mode 100644 skills/base44-platform-games/references/phaser/tweens.md create mode 100644 skills/base44-platform-games/references/threejs/assets_models_textures.md create mode 100644 skills/base44-platform-games/references/threejs/cameras.md create mode 100644 skills/base44-platform-games/references/threejs/character_controller.md create mode 100644 skills/base44-platform-games/references/threejs/enemies_and_movement.md create mode 100644 skills/base44-platform-games/references/threejs/game_state_and_flow.md create mode 100644 skills/base44-platform-games/references/threejs/input_and_controls.md create mode 100644 skills/base44-platform-games/references/threejs/performance.md create mode 100644 skills/base44-platform-games/references/threejs/physics_dynamics.md create mode 100644 skills/base44-platform-games/references/threejs/projectiles_and_pooling.md create mode 100644 skills/base44-platform-games/references/threejs/scene_and_rendering.md create mode 100644 skills/base44-platform-games/references/threejs/text_hud_ui.md create mode 100644 skills/base44-platform-games/references/threejs/triggers_and_collisions.md create mode 100644 skills/base44-platform-games/references/threejs/vfx_and_juice.md create mode 100644 skills/base44-platform-integrations-sdk/SKILL.md create mode 100644 skills/base44-platform-integrations-sdk/references/mobile.md create mode 100644 skills/base44-platform-integrations-sdk/references/web.md create mode 100644 skills/base44-platform-manifest.json create mode 100644 skills/base44-platform-platform/SKILL.md create mode 100644 skills/base44-platform-platform/references/game.md create mode 100644 skills/base44-platform-platform/references/mobile.md create mode 100644 skills/base44-platform-platform/references/web-spa.md create mode 100644 skills/base44-platform-platform/references/web-tanstack.md create mode 100644 skills/base44-platform-workflows/SKILL.md create mode 100644 skills/base44-platform-workflows/references/automations-legacy.md create mode 100644 skills/base44-platform-workflows/references/mobile-automations.md create mode 100644 skills/base44-platform-workflows/references/web-spa.md create mode 100644 skills/base44-platform-workflows/references/web-tanstack.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index f4be24a..ec2496a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -9,6 +9,30 @@ "homepage": "https://docs.base44.com", "repository": "https://github.com/base44/skills", "license": "MIT", - "keywords": ["base44", "full-stack", "sdk", "cli", "deployment", "entities", "backend-functions", "javascript", "typescript"], - "skills": ["./skills/base44-cli/", "./skills/base44-sdk/", "./skills/base44-troubleshooter/"] + "keywords": [ + "base44", + "full-stack", + "sdk", + "cli", + "deployment", + "entities", + "backend-functions", + "javascript", + "typescript" + ], + "skills": [ + "./skills/base44-cli/", + "./skills/base44-sdk/", + "./skills/base44-troubleshooter/", + "./skills/base44-platform-platform/", + "./skills/base44-platform-entities/", + "./skills/base44-platform-backend-functions/", + "./skills/base44-platform-workflows/", + "./skills/base44-platform-agents/", + "./skills/base44-platform-actors/", + "./skills/base44-platform-integrations-sdk/", + "./skills/base44-platform-connectors/", + "./skills/base44-platform-frontend/", + "./skills/base44-platform-games/" + ] } diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 7be8419..10578d0 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -4,14 +4,37 @@ "version": "1.0.0-beta.1", "description": "Build and deploy Base44 full-stack apps with CLI project management and JavaScript/TypeScript SDK development skills", "author": { - "name": "base44", - "url": "https://base44.com" + "name": "base44", + "url": "https://base44.com" }, "homepage": "https://docs.base44.com", "repository": "https://github.com/base44/skills", "license": "MIT", "logo": "./assets/base44-logo.png", - "keywords": ["base44", "full-stack", "sdk", "cli", "deployment", "entities", "backend-functions", "javascript", "typescript"], - "skills": ["./skills/base44-cli/", "./skills/base44-sdk/", "./skills/base44-troubleshooter/"] - } - \ No newline at end of file + "keywords": [ + "base44", + "full-stack", + "sdk", + "cli", + "deployment", + "entities", + "backend-functions", + "javascript", + "typescript" + ], + "skills": [ + "./skills/base44-cli/", + "./skills/base44-sdk/", + "./skills/base44-troubleshooter/", + "./skills/base44-platform-platform/", + "./skills/base44-platform-entities/", + "./skills/base44-platform-backend-functions/", + "./skills/base44-platform-workflows/", + "./skills/base44-platform-agents/", + "./skills/base44-platform-actors/", + "./skills/base44-platform-integrations-sdk/", + "./skills/base44-platform-connectors/", + "./skills/base44-platform-frontend/", + "./skills/base44-platform-games/" + ] +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ce1623e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,9 @@ +# The compiled platform skills are generated from the Base44 builder's prompt tree in +# base44-dev/apper and synced here nightly (CONTRIBUTING.md, "Compiled platform skills"). +# A hand edit here is overwritten by the next sync and fails the validator's manifest check, +# so the owners review the sync PRs and route content changes to the apper prompt partials. +/skills/base44-platform-*/ @base44/devs +/APPER_COMMIT @base44/devs +/scripts/validate-base44-platform.mjs @base44/devs +/.github/workflows/validate-base44-platform.yml @base44/devs +/.github/workflows/auto-merge-skill-update.yml @base44/devs diff --git a/.github/workflows/auto-merge-skill-update.yml b/.github/workflows/auto-merge-skill-update.yml new file mode 100644 index 0000000..4e150c9 --- /dev/null +++ b/.github/workflows/auto-merge-skill-update.yml @@ -0,0 +1,43 @@ +name: Auto-merge compiled skill updates + +# A nightly sync PR from base44-dev/apper (label `auto-skill-update`, opened by the sync app) that +# touches only the compiled `skills/base44-platform-*` tree and the APPER_COMMIT pin is merged +# automatically once `Validate base44-platform skills` passes. Anything else with the label (the +# CLI / SDK sync PRs, a hand-made PR) is left for a human. +# +# Requires: GitHub auto-merge enabled on this repository, the validator as a required check on +# main, and `vars.SKILLS_SYNC_BOT_LOGIN` set to the sync app's login (for example +# `base44-skills-sync[bot]`). See CONTRIBUTING.md, "Compiled platform skills". + +on: + pull_request: + types: [labeled, synchronize, reopened] + +permissions: + contents: write + pull-requests: write + +jobs: + enable-auto-merge: + if: contains(github.event.pull_request.labels.*.name, 'auto-skill-update') && github.event.pull_request.user.login == vars.SKILLS_SYNC_BOT_LOGIN + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + - name: Only the compiled tree and the pin may change + env: + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + git diff --name-only "$BASE" "$HEAD" > changed.txt + if grep -vE '^(skills/base44-platform-[^/]+/|APPER_COMMIT$)' changed.txt; then + echo "::error::the PR changes files outside the compiled tree; leaving it for a human"; exit 1 + fi + echo "$(wc -l < changed.txt) generated files changed" + - name: Enable auto-merge + env: + GH_TOKEN: ${{ github.token }} + run: gh pr merge "${{ github.event.pull_request.number }}" --squash --auto diff --git a/.github/workflows/notify-slack-skill-sync.yml b/.github/workflows/notify-slack-skill-sync.yml index 43ba011..8d2a539 100644 --- a/.github/workflows/notify-slack-skill-sync.yml +++ b/.github/workflows/notify-slack-skill-sync.yml @@ -1,11 +1,12 @@ name: Notify Slack on skill-sync PRs -# When an auto-generated base44-cli / base44-sdk skill-sync PR opens, post a +# When an auto-generated skill-sync PR opens (base44-cli / base44-sdk, or the nightly +# base44-platform-* tree compiled from base44-dev/apper), post a # message to the team Slack channel so someone remembers to review it. Fires # only for skill-update PRs; anything else is ignored. # # Discriminator: the `auto-skill-update` label. The sync workflows -# (sync-cli-skill.yml / sync-sdk-skill.yml) create their PRs with this label, +# (sync-cli-skill.yml / sync-sdk-skill.yml here, agent-skills-sync.yml in apper) create their PRs with this label, # so it is present in the `opened` event payload. This is a stable, explicit # contract — independent of the PR title wording or which bot authored it. # diff --git a/.github/workflows/validate-base44-platform.yml b/.github/workflows/validate-base44-platform.yml new file mode 100644 index 0000000..484cb65 --- /dev/null +++ b/.github/workflows/validate-base44-platform.yml @@ -0,0 +1,28 @@ +name: Validate base44-platform skills + +# The `skills/base44-platform-*` tree is compiled by base44-dev/apper (backend/scripts/agent_skills) +# and synced here nightly. This check proves a PR carries exactly what the compiler emitted: +# headers, manifest hashes, frontmatter, links and the APPER_COMMIT pin. Make it a required check +# on main, so the auto-merge workflow only merges a tree that passed it. + +on: + pull_request: + paths: + - "skills/base44-platform-*/**" + - "APPER_COMMIT" + - "scripts/validate-base44-platform.mjs" + - ".github/workflows/validate-base44-platform.yml" + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@v6 + with: + node-version: '24' + - run: node scripts/validate-base44-platform.mjs diff --git a/APPER_COMMIT b/APPER_COMMIT new file mode 100644 index 0000000..a80d41f --- /dev/null +++ b/APPER_COMMIT @@ -0,0 +1 @@ +a513e8922412ce0b28bbe6e762108f29fbdb2b56 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11a4a3a..22bffa8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,6 +117,68 @@ npx skills add . --skill your-skill-name -a cursor - Prefer modern JavaScript/TypeScript syntax - Follow Base44 SDK conventions +## Compiled platform skills (`base44-platform-*`) + +The `skills/base44-platform-*` directories are **generated**. Their source is the Base44 builder's +system prompt tree in [base44-dev/apper](https://github.com/base44-dev/apper) +(`backend/app/prompts/`), rendered for an external coding agent by the compiler in +`backend/scripts/agent_skills` and published here by apper's nightly `agent-skills-sync` workflow. +They describe the platform exactly as the in-editor AI learns it, one reference per app shape. + +### How the pipeline works + +1. **Nightly, in apper**: the compiler renders every prompt partial in the capability matrix + (`backend/app/user_apps/agent_skills/matrix.py`) with `surface="external"`, validates the result + (deterministic output, no builder-tool vocabulary, manifest, links) and opens one PR here on the + branch `auto/base44-platform-skills`, labeled `auto-skill-update`. A night with no prompt change + opens nothing. +2. **In this repo**: `Validate base44-platform skills` (`scripts/validate-base44-platform.mjs`) + checks that the PR carries exactly what the compiler emitted: the generated-file header on every + file, sha256 hashes matching `skills/base44-platform-manifest.json`, frontmatter whose `name` + equals the directory name, resolving links, and a 40-hex `APPER_COMMIT` pin. +3. `Auto-merge compiled skill updates` enables squash auto-merge on a labeled PR opened by the sync + app whose diff is confined to `skills/base44-platform-*/**` and `APPER_COMMIT`. Everything else + waits for a human. The Slack notifier posts the PR like any other skill-sync PR. + +### How to change a compiled skill + +Do not edit these files here: the validator fails on a hand edit, and the next sync overwrites it. +Change the source instead: + +- **Wording that is wrong for every agent** (builder and external alike): edit the prompt partial in + apper under `backend/app/prompts/`. The nightly sync carries it here. +- **Wording that only an external agent needs** (a builder-tool instruction with no external + equivalent, a pointer to the MCP / CLI surface): use the `external` branch inside the partial, + `{% if external %}…{% else %}…{% endif %}`. The builder's render stays byte-identical and is + pinned by hash tests. The convention is documented in apper's `backend/app/prompts/CLAUDE.md` + (Rule 4). +- **A new capability, app shape or reference file**: add it to the matrix in apper and classify the + partial there. Nothing in this repo needs to change; the sync adds the directory. +- **A flag that reached 100%** but still gates prompt text: add it to + `backend/app/user_apps/agent_skills/shipped_overrides.json` in apper, so the skills describe the + shipped arm. + +`APPER_COMMIT` names the apper commit the current tree was compiled from; the sync PR body lists +the prompt-source commits since the previous pin. + +### Which reference an agent should read + +Each `base44-platform-*` skill's `SKILL.md` routes by app profile (Vite SPA, TanStack Start, native +mobile, 2D or 3D game). The platform answers that question directly: +`GET /api/apps/{app_id}/agent-profile` (public API, also reachable through the Base44 MCP +`execute_api` tool) returns the app's shape, the workspace's entitlements and the exact reference +paths to read. + +### Operating the pipeline (maintainers) + +- The sync PR is opened with a GitHub App installed on this repository (contents and pull requests, + write). Apper reads it from `vars.BASE44_SKILLS_APP_ID` and `secrets.BASE44_SKILLS_APP_PRIVATE_KEY`; + this repo needs `vars.SKILLS_SYNC_BOT_LOGIN` set to the app's login so auto-merge recognizes its PRs. +- Auto-merge must be enabled in the repository settings, with `Validate base44-platform skills` as + a required check on `main`. +- `.github/CODEOWNERS` routes the generated tree and the two workflows to the owning team. +- To rerun a sync by hand, dispatch apper's `Agent skills sync` workflow (it has a dry-run input). + ## Questions? - Check existing skills for examples diff --git a/README.md b/README.md index 679c133..ae68772 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,20 @@ npx skills add base44/skills --skill base44-remote-dev --skill base44-sandbox -- | [`base44-troubleshooter`](skills/base44-troubleshooter/SKILL.md) | Troubleshoot production issues using backend function logs. Use when investigating app errors or diagnosing production problems. | | [`base44-remote-dev`](skills/base44-remote-dev/SKILL.md) | Develop a Base44 app remotely from your own coding agent by connecting it to the Base44 sandbox over MCP or the `base44 sandbox` CLI. | | [`base44-sandbox`](skills/base44-sandbox/SKILL.md) | Author Base44 app code inside the cloud sandbox — no deploy/push; writing a resource file (function, actor, entity, agent) into the sandbox is what ships it. | +| [`base44-platform-platform`](skills/base44-platform-platform/SKILL.md) | Platform concepts for a Base44 app: entities, functions or server code, workflows, file structure, authentication and user invites, per app type. Compiled nightly from the Base44 builder's own prompt. | +| [`base44-platform-entities`](skills/base44-platform-entities/SKILL.md) | Entity schemas, the entity SDK and row-level security rules, as the Base44 builder teaches them. Compiled nightly from the Base44 builder's own prompt. | +| [`base44-platform-backend-functions`](skills/base44-platform-backend-functions/SKILL.md) | Backend functions on Cloudflare (or TanStack Start server code): secrets, the SDK on the server, auth, webhooks, testing, the InvokeLLM model table. Compiled nightly from the Base44 builder's own prompt. | +| [`base44-platform-workflows`](skills/base44-platform-workflows/SKILL.md) | Workflows: trigger-driven `.jsonc` processes under `base44/workflows/` with the full authoring guide, plus the legacy automations pipeline. Compiled nightly from the Base44 builder's own prompt. | +| [`base44-platform-agents`](skills/base44-platform-agents/SKILL.md) | In-app AI agents: config files, tool permissions, channels, the agents SDK and code agents on the AI gateway. Compiled nightly from the Base44 builder's own prompt. | +| [`base44-platform-actors`](skills/base44-platform-actors/SKILL.md) | Realtime actors: stateful WebSocket rooms for multiplayer, presence, collaborative boards and chat. Compiled nightly from the Base44 builder's own prompt. | +| [`base44-platform-integrations-sdk`](skills/base44-platform-integrations-sdk/SKILL.md) | Built-in integrations (InvokeLLM, SendEmail, file uploads, data extraction) with every Core endpoint schema, plus analytics. Compiled nightly from the Base44 builder's own prompt. | +| [`base44-platform-connectors`](skills/base44-platform-connectors/SKILL.md) | App connectors: OAuth integrations in shared, app-user and BYO-shared modes, and how server code reads their tokens. Compiled nightly from the Base44 builder's own prompt. | +| [`base44-platform-frontend`](skills/base44-platform-frontend/SKILL.md) | Frontend coding standards: conventions, installed packages, routing, Tailwind and design tokens, npm installs, per framework. Compiled nightly from the Base44 builder's own prompt. | +| [`base44-platform-games`](skills/base44-platform-games/SKILL.md) | Phaser 4 and Three.js game references: architecture, assets, analytics and per-topic engine guides. Compiled nightly from the Base44 builder's own prompt. | + +### Platform skills (compiled) + +The `base44-platform-*` skills are generated from the Base44 builder's own system prompt by [base44-dev/apper](https://github.com/base44-dev/apper) and synced here nightly, so they describe the platform exactly as the in-editor AI learns it: one reference per app shape (Vite SPA, TanStack Start, native mobile, 2D/3D game). Ask the platform which shape an app is with `GET /api/apps/{app_id}/agent-profile` (also reachable through the Base44 MCP `execute_api` tool); it returns the exact reference files to read. Do not edit these files by hand: the `APPER_COMMIT` file records the apper commit they were compiled from, and the next sync overwrites them. How the pipeline works and how to change the content: [CONTRIBUTING.md](CONTRIBUTING.md#compiled-platform-skills-base44-platform-). ## About Agent Skills diff --git a/plugins/base44-sandbox/.claude-plugin/plugin.json b/plugins/base44-sandbox/.claude-plugin/plugin.json index 41d5c97..973489b 100644 --- a/plugins/base44-sandbox/.claude-plugin/plugin.json +++ b/plugins/base44-sandbox/.claude-plugin/plugin.json @@ -9,6 +9,28 @@ "homepage": "https://docs.base44.com", "repository": "https://github.com/base44/skills", "license": "MIT", - "keywords": ["base44", "sandbox", "remote-dev", "mcp", "entities", "backend-functions"], - "skills": ["./skills/base44-remote-dev/", "./skills/base44-sandbox/", "./skills/base44-sdk/", "./skills/base44-troubleshooter/"] + "keywords": [ + "base44", + "sandbox", + "remote-dev", + "mcp", + "entities", + "backend-functions" + ], + "skills": [ + "./skills/base44-remote-dev/", + "./skills/base44-sandbox/", + "./skills/base44-sdk/", + "./skills/base44-troubleshooter/", + "./skills/base44-platform-platform/", + "./skills/base44-platform-entities/", + "./skills/base44-platform-backend-functions/", + "./skills/base44-platform-workflows/", + "./skills/base44-platform-agents/", + "./skills/base44-platform-actors/", + "./skills/base44-platform-integrations-sdk/", + "./skills/base44-platform-connectors/", + "./skills/base44-platform-frontend/", + "./skills/base44-platform-games/" + ] } diff --git a/plugins/base44-sandbox/.codex-plugin/plugin.json b/plugins/base44-sandbox/.codex-plugin/plugin.json index 76d955f..21ab30f 100644 --- a/plugins/base44-sandbox/.codex-plugin/plugin.json +++ b/plugins/base44-sandbox/.codex-plugin/plugin.json @@ -9,15 +9,41 @@ "homepage": "https://docs.base44.com", "repository": "https://github.com/base44/skills", "license": "MIT", - "keywords": ["base44", "sandbox", "remote-dev", "mcp", "entities", "backend-functions"], - "skills": ["./skills/base44-remote-dev/", "./skills/base44-sandbox/", "./skills/base44-sdk/", "./skills/base44-troubleshooter/"], + "keywords": [ + "base44", + "sandbox", + "remote-dev", + "mcp", + "entities", + "backend-functions" + ], + "skills": [ + "./skills/base44-remote-dev/", + "./skills/base44-sandbox/", + "./skills/base44-sdk/", + "./skills/base44-troubleshooter/", + "./skills/base44-platform-platform/", + "./skills/base44-platform-entities/", + "./skills/base44-platform-backend-functions/", + "./skills/base44-platform-workflows/", + "./skills/base44-platform-agents/", + "./skills/base44-platform-actors/", + "./skills/base44-platform-integrations-sdk/", + "./skills/base44-platform-connectors/", + "./skills/base44-platform-frontend/", + "./skills/base44-platform-games/" + ], "interface": { "displayName": "Base44 Sandbox", "shortDescription": "Develop Base44 apps remotely in the cloud sandbox from Codex", "longDescription": "Develop a Base44 app inside Base44's cloud sandbox where the platform auto-builds, auto-commits, and auto-syncs. Includes conventions for entities, functions, agents, and connectors, SDK development, and production troubleshooting — without deploy/push commands.", "developerName": "base44", "category": "Coding", - "capabilities": ["Interactive", "Read", "Write"], + "capabilities": [ + "Interactive", + "Read", + "Write" + ], "websiteURL": "https://base44.com", "logo": "./assets/base44-logo.png" } diff --git a/plugins/base44-sandbox/.cursor-plugin/plugin.json b/plugins/base44-sandbox/.cursor-plugin/plugin.json index 060b8cf..ff15fa1 100644 --- a/plugins/base44-sandbox/.cursor-plugin/plugin.json +++ b/plugins/base44-sandbox/.cursor-plugin/plugin.json @@ -4,13 +4,35 @@ "version": "1.0.0-beta.1", "description": "Develop a Base44 app remotely in Base44's cloud sandbox; conventions only, no deploy/push.", "author": { - "name": "base44", - "url": "https://base44.com" + "name": "base44", + "url": "https://base44.com" }, "homepage": "https://docs.base44.com", "repository": "https://github.com/base44/skills", "license": "MIT", "logo": "./assets/base44-logo.png", - "keywords": ["base44", "sandbox", "remote-dev", "mcp", "entities", "backend-functions"], - "skills": ["./skills/base44-remote-dev/", "./skills/base44-sandbox/", "./skills/base44-sdk/", "./skills/base44-troubleshooter/"] - } + "keywords": [ + "base44", + "sandbox", + "remote-dev", + "mcp", + "entities", + "backend-functions" + ], + "skills": [ + "./skills/base44-remote-dev/", + "./skills/base44-sandbox/", + "./skills/base44-sdk/", + "./skills/base44-troubleshooter/", + "./skills/base44-platform-platform/", + "./skills/base44-platform-entities/", + "./skills/base44-platform-backend-functions/", + "./skills/base44-platform-workflows/", + "./skills/base44-platform-agents/", + "./skills/base44-platform-actors/", + "./skills/base44-platform-integrations-sdk/", + "./skills/base44-platform-connectors/", + "./skills/base44-platform-frontend/", + "./skills/base44-platform-games/" + ] +} diff --git a/scripts/validate-base44-platform.mjs b/scripts/validate-base44-platform.mjs new file mode 100644 index 0000000..101bae3 --- /dev/null +++ b/scripts/validate-base44-platform.mjs @@ -0,0 +1,88 @@ +#!/usr/bin/env node +// Validates the compiled `skills/base44-platform-*` tree that base44-dev/apper publishes nightly. +// The compiler (apper: backend/scripts/agent_skills) owns the content; this proves a PR carries +// exactly what it emitted: every generated file bears the header, is listed in the manifest with +// a matching sha256, has valid frontmatter, and links only to files that exist. + +import { createHash } from "node:crypto"; +import { promises as fs } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +const repoRoot = process.cwd(); +const skillsDir = path.join(repoRoot, "skills"); +const PREFIX = "base44-platform-"; +const MANIFEST = path.join(skillsDir, `${PREFIX}manifest.json`); +const HEADER = " +# Base44 actors + +One actor instance per room id; the actor is authoritative, clients send operations. Writing the entry file deploys the actor. + +> **Requires the Builder plan or above.** The workspace's entitlement is reported by the agent-profile endpoint; on a lower plan the platform rejects the deploy. + +## Pick the reference for this app + +Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (over the Base44 MCP `execute_api` +tool, or with your Base44 CLI token); it returns the framework, app type, runtime, +entitlements and the exact reference files to read. Without it, detect from the project files: +`src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a +native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. + +| Profile | Read | +|---|---| +| Web app, Vite SPA (the default web template) | [Realtime actors (web apps)](references/web.md) | +| Web app, TanStack Start | [Realtime actors (web apps)](references/web.md) | +| Native mobile app (Expo, React Native) | [Realtime actors (mobile apps)](references/mobile.md) | +| Game, 2D (Phaser 4) | [Realtime actors (web apps)](references/web.md) | +| Game, 3D (Three.js) | [Realtime actors (web apps)](references/web.md) | diff --git a/skills/base44-platform-actors/references/mobile.md b/skills/base44-platform-actors/references/mobile.md new file mode 100644 index 0000000..558deba --- /dev/null +++ b/skills/base44-platform-actors/references/mobile.md @@ -0,0 +1,122 @@ + +# Base44 actors: Realtime actors (mobile apps) + + +Realtime shared state runs on Actors: stateful server rooms over WebSockets (Durable Objects). One live instance per room id — every client connected to that id shares it. Use an actor whenever users interact live in one shared session: collaborative drawing and whiteboards, shared docs, presence and live cursors, chat rooms, live auctions, multiplayer games. Not for single-user state, not for a page that just lists records live (entity realtime subscriptions already push record changes), not for async flows (entities + functions are enough). The actor is AUTHORITATIVE: clients send inputs/operations, the actor validates, applies, and broadcasts — never trust client-computed outcomes, and never advance authoritative state client-side. + + +Write `base44/actors/{Name}/entry.ts` — the FOLDER names the actor (it is what `base44.actors.{Name}` resolves); export the class as the DEFAULT export. One actor per directory. Author plain JS like a backend function — no type annotations; the file is `.ts` only for the entry contract. Writing the file deploys it; there is no request-style test for an actor (WebSocket) — verify in the preview. + +```js +// base44/actors/BoardRoom/entry.ts (plain JS) +import { Actor } from 'base44:runtime/actors'; // the ONLY import that resolves the Actor base + +const MAX_USERS = 32; // a room is single-threaded — admit only what one instance can serve + +export default class BoardRoom extends Actor { + // Keyed by conn.id (the SDK reuses it on reconnect, so entries are reclaimable) — + // but conn.id is CLIENT-HELD and must NEVER appear in broadcasts: broadcast state + // is keyed by the server-assigned seat instead. + users = new Map(); // conn.id -> { seat, cursor } + nextSeat = 1; + items = new Map(); // the shared, persisted room state + + async handleStart() { + // Runs on ANY wake (deploy restart, idle, hibernation) — rehydrate, then + // reconcile: a hibernation wake keeps sockets ATTACHED without re-running + // handleConnect, so a still-live id must keep its seat, not lose it. + const saved = await this.storage.get('items'); + if (saved) this.items = new Map(saved); + const seats = await this.storage.get('seats'); + if (seats) this.users = new Map(seats); + const live = new Set(this.getConnections().map((c) => c.id)); + for (const id of this.users.keys()) if (!live.has(id)) this.users.delete(id); + this.nextSeat = Math.max(0, ...[...this.users.values()].map((u) => u.seat)) + 1; + } + async handleConnect(conn) { + if (!this.users.has(conn.id) && this.users.size >= MAX_USERS) { + conn.reject(4001, 'room full'); // reject closes the socket but NOT this handler — + return; // return immediately + } + // Reconnects are ROUTINE (blips, reloads, redeploys): a returning id reclaims + // its entry — never demote it or mint a new seat. + if (!this.users.has(conn.id)) { + this.users.set(conn.id, { seat: this.nextSeat++, cursor: null }); + await this.saveSeats(); // at seat milestones, never per cursor move + } + conn.send({ type: 'you', seat: this.users.get(conn.id).seat }); + conn.send({ type: 'state', items: [...this.items.values()] }); // late joiners get full state + this.broadcastPresence(); + } + async handleMessage(conn, msg) { + // Runtime-validate EVERYTHING: the wire payload is attacker-controlled and + // msg can even be null. Accept operations, never authoritative state. + if (typeof msg !== 'object' || msg === null) return; + const u = this.users.get(conn.id); + if (!u) return; + if (msg.type === 'cursor') { + u.cursor = [Number(msg.x) || 0, Number(msg.y) || 0]; + this.broadcastPresence(); + } else if (msg.type === 'upsert_item' && typeof msg.id === 'string' && msg.id.length <= 64) { + const item = { id: msg.id, text: String(msg.text ?? '').slice(0, 2000) }; + this.items.set(item.id, item); + await this.storage.put('items', [...this.items.entries()]); // persist on change — never on every pointer move + this.broadcast({ type: 'item', item }); + } + } + async handleClose(conn) { + this.users.delete(conn.id); + await this.saveSeats(); + this.broadcastPresence(); + } + saveSeats() { return this.storage.put('seats', [...this.users.entries()]); } + broadcastPresence() { + // PROJECT an explicit public shape — never spread whole server objects into a + // broadcast (they grow per-user secrets later). + this.broadcast({ + type: 'presence', + users: [...this.users.values()].map((u) => ({ seat: u.seat, cursor: u.cursor })), + }); + } +} +``` + + + +- Handlers: `handleConnect` / `handleMessage` / `handleClose`, plus optional `handleStart` (any wake) and `handleWake(key)` (scheduled wakes). Never override `onStart` or `onAlarm` — platform plumbing. +- State: instance fields reset when the room idles out — a room hibernates after ~10s of quiet EVEN with clients connected; `this.storage` survives. Persist state you can't lose when it changes and rehydrate it in `handleStart()`; never write high-frequency churn (every pointer move, every keystroke) to storage. A hibernation wake keeps sockets ATTACHED and does NOT re-run `handleConnect`, so reconcile the rehydrated state against `this.getConnections()` — skip that and every connected client stays unrecognized until it reconnects. (`static options = { hibernate: false }` only makes the room non-hibernatable, NOT resident: it is still evicted after ~1-2 minutes idle, so storage is still the only durable answer. Rarely needed.) +- `conn.id` is the per-connection identity. The SDK reuses it across auto-reconnects, and a persisted `connect({ id })` (see the client) makes reloads reclaim it too — key seats/roles by it and let a returning id RECLAIM its entry with its role and work intact. When a drop shouldn't discard that work, give the id a short grace before wiping its state; when the LAST client leaves mid-session, schedule the cleanup as a wake and cancel it on reconnect. conn.id is client-held: fine for seats and reconnects, NEVER attribution — that is `conn.identity`, platform-verified: `{ type: 'authenticated', userId }` or `{ type: 'anonymous', anonymousId }` (a visitor, not a user). +- Scheduled wakes: `this.schedule(key, atEpochMs)` persists a named wake; `handleWake(key)` runs at/after it EVEN IF THE ROOM IS EMPTY AND ASLEEP. `cancelSchedule(key)` cancels; re-scheduling a key overwrites. Coarse (±seconds) and fires at most once — use for deadline timers (auction close, session expiry), delayed cleanup of abandoned rooms, absolute-time events; in-session countdowns stay timestamp-driven. +- `this.broadcast(...)` is for ROOM-wide state; events about ONE client (your assigned color, your validation error, your private draft) go `conn.send(...)` — broadcasting them leaks private state and makes every client react. Messages are JSON both ways; `type` values starting with `__` are platform-reserved. +- `this.client` is a ready-made `@base44/sdk` client on every actor (`await this.client.functions.invoke('name', input)` — there is no `functions.name()` form, and the returned value sits on `.data`; `await this.client.entities.Thing.filter(...)`) acting as the app's ANONYMOUS role, pinned to the actor's own deploy. `this.client.asServiceRole` (`entities`, `functions`, `integrations`) is the admin-level client, as in a backend function: use it for work the ROOM owns and has validated — persisting its durable result, maintaining its registry row, reading private config — never to do what a client asked without validating it, and never loosen RLS to the anonymous role instead. Neither client is a signed-in user: attribute service-role records with an explicit field from `conn.identity.userId` (authenticated identities only). The first privileged call after a wake pays a token exchange — keep it on persist paths, not per message. +- Durable results (the finished drawing, a chat transcript, an exported document): the actor broadcasts the authoritative result, writes it to `this.storage`, and persists it to entities via `asServiceRole`, keyed by the room instance id and IDEMPOTENT (check for the existing record before creating; readers treat the earliest record per key as canonical) — delivery and wakes are at-least-once. Re-`conn.send` it to (re)connecting clients: shells cannot read actor storage. The shell writes only records the USER owns, never the room's canonical result. +- Actor auth is automatic: the first actor deploy generates an `ACTOR_TOKEN_SECRET` app secret (one per app, shared by all actors) that seeds the connection-token keys. Never ask the user for it or set it yourself; overwriting it rotates the keys and redeploys the actors; it is not readable from function or actor env. +- Rooms and discovery: one room = one session (a drawing board, a chat channel, a document) — never funnel everyone into a single global room. For browsable rooms keep a registry ENTITY (e.g. Room: status, user_count) whose record id IS the actor instance id; the registry is advertising, the actor is truth — admission is decided in `handleConnect`, never from the row. List rooms by subscribing to the entity FIRST, then fetching and reconciling by id; filter to recently-updated rows (crashed rooms leave stale ones). PRIVATE rooms: there is no room-level auth yet — the instance id is the only admission control, so mint it with `crypto.randomUUID()` (record ids are enumerable ObjectIds), never list it in a readable registry, and share it only as an invite link/code. + + + +```js +// React shell. Connect in useEffect WITH cleanup — a bare connect in the component +// body leaks a socket per render. +import { base44 } from '@/api/base44Client'; + +useEffect(() => { + // Persist the conn id per tab so a page RELOAD reclaims the same seat; a fresh + // per-render id would leak seats on every refresh. + let connId = sessionStorage.getItem('connId'); + if (!connId) { connId = crypto.randomUUID(); sessionStorage.setItem('connId', connId); } + const room = base44.actors.BoardRoom(roomId).connect({ id: connId }); // the handle IS the connection + const sub = room.subscribe((msg) => { + // Branch on msg.type into React state; drop unknown types. + }); + return () => { sub.unsubscribe(); room.close(); }; +}, [roomId]); +// Send operations on change (room.send({ type: 'upsert_item', ... })); throttle +// high-frequency input like cursors or brush strokes (~20-30/s max), never per render. +``` +Reconnection, heartbeats, and auth are handled by the SDK. +React Native: the same SDK API applies; there is no `sessionStorage` — keep the conn id in module scope or state (a fresh launch mints a new identity). + + diff --git a/skills/base44-platform-actors/references/web.md b/skills/base44-platform-actors/references/web.md new file mode 100644 index 0000000..058bbc5 --- /dev/null +++ b/skills/base44-platform-actors/references/web.md @@ -0,0 +1,121 @@ + +# Base44 actors: Realtime actors (web apps) + + +Realtime shared state runs on Actors: stateful server rooms over WebSockets (Durable Objects). One live instance per room id — every client connected to that id shares it. Use an actor whenever users interact live in one shared session: collaborative drawing and whiteboards, shared docs, presence and live cursors, chat rooms, live auctions, multiplayer games. Not for single-user state, not for a page that just lists records live (entity realtime subscriptions already push record changes), not for async flows (entities + functions are enough). The actor is AUTHORITATIVE: clients send inputs/operations, the actor validates, applies, and broadcasts — never trust client-computed outcomes, and never advance authoritative state client-side. + + +Write `base44/actors/{Name}/entry.ts` — the FOLDER names the actor (it is what `base44.actors.{Name}` resolves); export the class as the DEFAULT export. One actor per directory. Author plain JS like a backend function — no type annotations; the file is `.ts` only for the entry contract. Writing the file deploys it; there is no request-style test for an actor (WebSocket) — verify in the preview. + +```js +// base44/actors/BoardRoom/entry.ts (plain JS) +import { Actor } from 'base44:runtime/actors'; // the ONLY import that resolves the Actor base + +const MAX_USERS = 32; // a room is single-threaded — admit only what one instance can serve + +export default class BoardRoom extends Actor { + // Keyed by conn.id (the SDK reuses it on reconnect, so entries are reclaimable) — + // but conn.id is CLIENT-HELD and must NEVER appear in broadcasts: broadcast state + // is keyed by the server-assigned seat instead. + users = new Map(); // conn.id -> { seat, cursor } + nextSeat = 1; + items = new Map(); // the shared, persisted room state + + async handleStart() { + // Runs on ANY wake (deploy restart, idle, hibernation) — rehydrate, then + // reconcile: a hibernation wake keeps sockets ATTACHED without re-running + // handleConnect, so a still-live id must keep its seat, not lose it. + const saved = await this.storage.get('items'); + if (saved) this.items = new Map(saved); + const seats = await this.storage.get('seats'); + if (seats) this.users = new Map(seats); + const live = new Set(this.getConnections().map((c) => c.id)); + for (const id of this.users.keys()) if (!live.has(id)) this.users.delete(id); + this.nextSeat = Math.max(0, ...[...this.users.values()].map((u) => u.seat)) + 1; + } + async handleConnect(conn) { + if (!this.users.has(conn.id) && this.users.size >= MAX_USERS) { + conn.reject(4001, 'room full'); // reject closes the socket but NOT this handler — + return; // return immediately + } + // Reconnects are ROUTINE (blips, reloads, redeploys): a returning id reclaims + // its entry — never demote it or mint a new seat. + if (!this.users.has(conn.id)) { + this.users.set(conn.id, { seat: this.nextSeat++, cursor: null }); + await this.saveSeats(); // at seat milestones, never per cursor move + } + conn.send({ type: 'you', seat: this.users.get(conn.id).seat }); + conn.send({ type: 'state', items: [...this.items.values()] }); // late joiners get full state + this.broadcastPresence(); + } + async handleMessage(conn, msg) { + // Runtime-validate EVERYTHING: the wire payload is attacker-controlled and + // msg can even be null. Accept operations, never authoritative state. + if (typeof msg !== 'object' || msg === null) return; + const u = this.users.get(conn.id); + if (!u) return; + if (msg.type === 'cursor') { + u.cursor = [Number(msg.x) || 0, Number(msg.y) || 0]; + this.broadcastPresence(); + } else if (msg.type === 'upsert_item' && typeof msg.id === 'string' && msg.id.length <= 64) { + const item = { id: msg.id, text: String(msg.text ?? '').slice(0, 2000) }; + this.items.set(item.id, item); + await this.storage.put('items', [...this.items.entries()]); // persist on change — never on every pointer move + this.broadcast({ type: 'item', item }); + } + } + async handleClose(conn) { + this.users.delete(conn.id); + await this.saveSeats(); + this.broadcastPresence(); + } + saveSeats() { return this.storage.put('seats', [...this.users.entries()]); } + broadcastPresence() { + // PROJECT an explicit public shape — never spread whole server objects into a + // broadcast (they grow per-user secrets later). + this.broadcast({ + type: 'presence', + users: [...this.users.values()].map((u) => ({ seat: u.seat, cursor: u.cursor })), + }); + } +} +``` + + + +- Handlers: `handleConnect` / `handleMessage` / `handleClose`, plus optional `handleStart` (any wake) and `handleWake(key)` (scheduled wakes). Never override `onStart` or `onAlarm` — platform plumbing. +- State: instance fields reset when the room idles out — a room hibernates after ~10s of quiet EVEN with clients connected; `this.storage` survives. Persist state you can't lose when it changes and rehydrate it in `handleStart()`; never write high-frequency churn (every pointer move, every keystroke) to storage. A hibernation wake keeps sockets ATTACHED and does NOT re-run `handleConnect`, so reconcile the rehydrated state against `this.getConnections()` — skip that and every connected client stays unrecognized until it reconnects. (`static options = { hibernate: false }` only makes the room non-hibernatable, NOT resident: it is still evicted after ~1-2 minutes idle, so storage is still the only durable answer. Rarely needed.) +- `conn.id` is the per-connection identity. The SDK reuses it across auto-reconnects, and a persisted `connect({ id })` (see the client) makes reloads reclaim it too — key seats/roles by it and let a returning id RECLAIM its entry with its role and work intact. When a drop shouldn't discard that work, give the id a short grace before wiping its state; when the LAST client leaves mid-session, schedule the cleanup as a wake and cancel it on reconnect. conn.id is client-held: fine for seats and reconnects, NEVER attribution — that is `conn.identity`, platform-verified: `{ type: 'authenticated', userId }` or `{ type: 'anonymous', anonymousId }` (a visitor, not a user). +- Scheduled wakes: `this.schedule(key, atEpochMs)` persists a named wake; `handleWake(key)` runs at/after it EVEN IF THE ROOM IS EMPTY AND ASLEEP. `cancelSchedule(key)` cancels; re-scheduling a key overwrites. Coarse (±seconds) and fires at most once — use for deadline timers (auction close, session expiry), delayed cleanup of abandoned rooms, absolute-time events; in-session countdowns stay timestamp-driven. +- `this.broadcast(...)` is for ROOM-wide state; events about ONE client (your assigned color, your validation error, your private draft) go `conn.send(...)` — broadcasting them leaks private state and makes every client react. Messages are JSON both ways; `type` values starting with `__` are platform-reserved. +- `this.client` is a ready-made `@base44/sdk` client on every actor (`await this.client.functions.invoke('name', input)` — there is no `functions.name()` form, and the returned value sits on `.data`; `await this.client.entities.Thing.filter(...)`) acting as the app's ANONYMOUS role, pinned to the actor's own deploy. `this.client.asServiceRole` (`entities`, `functions`, `integrations`) is the admin-level client, as in a backend function: use it for work the ROOM owns and has validated — persisting its durable result, maintaining its registry row, reading private config — never to do what a client asked without validating it, and never loosen RLS to the anonymous role instead. Neither client is a signed-in user: attribute service-role records with an explicit field from `conn.identity.userId` (authenticated identities only). The first privileged call after a wake pays a token exchange — keep it on persist paths, not per message. +- Durable results (the finished drawing, a chat transcript, an exported document): the actor broadcasts the authoritative result, writes it to `this.storage`, and persists it to entities via `asServiceRole`, keyed by the room instance id and IDEMPOTENT (check for the existing record before creating; readers treat the earliest record per key as canonical) — delivery and wakes are at-least-once. Re-`conn.send` it to (re)connecting clients: shells cannot read actor storage. The shell writes only records the USER owns, never the room's canonical result. +- Actor auth is automatic: the first actor deploy generates an `ACTOR_TOKEN_SECRET` app secret (one per app, shared by all actors) that seeds the connection-token keys. Never ask the user for it or set it yourself; overwriting it rotates the keys and redeploys the actors; it is not readable from function or actor env. +- Rooms and discovery: one room = one session (a drawing board, a chat channel, a document) — never funnel everyone into a single global room. For browsable rooms keep a registry ENTITY (e.g. Room: status, user_count) whose record id IS the actor instance id; the registry is advertising, the actor is truth — admission is decided in `handleConnect`, never from the row. List rooms by subscribing to the entity FIRST, then fetching and reconciling by id; filter to recently-updated rows (crashed rooms leave stale ones). PRIVATE rooms: there is no room-level auth yet — the instance id is the only admission control, so mint it with `crypto.randomUUID()` (record ids are enumerable ObjectIds), never list it in a readable registry, and share it only as an invite link/code. + + + +```js +// React shell. Connect in useEffect WITH cleanup — a bare connect in the component +// body leaks a socket per render. +import { base44 } from '@/api/base44Client'; + +useEffect(() => { + // Persist the conn id per tab so a page RELOAD reclaims the same seat; a fresh + // per-render id would leak seats on every refresh. + let connId = sessionStorage.getItem('connId'); + if (!connId) { connId = crypto.randomUUID(); sessionStorage.setItem('connId', connId); } + const room = base44.actors.BoardRoom(roomId).connect({ id: connId }); // the handle IS the connection + const sub = room.subscribe((msg) => { + // Branch on msg.type into React state; drop unknown types. + }); + return () => { sub.unsubscribe(); room.close(); }; +}, [roomId]); +// Send operations on change (room.send({ type: 'upsert_item', ... })); throttle +// high-frequency input like cursors or brush strokes (~20-30/s max), never per render. +``` +Reconnection, heartbeats, and auth are handled by the SDK. + + diff --git a/skills/base44-platform-agents/SKILL.md b/skills/base44-platform-agents/SKILL.md new file mode 100644 index 0000000..fca6a73 --- /dev/null +++ b/skills/base44-platform-agents/SKILL.md @@ -0,0 +1,23 @@ +--- +name: base44-platform-agents +description: "Base44 in-app AI agents: JSON agent configs under base44/agents/ with entity, backend-function or server-route tools, WhatsApp and Telegram channels, the agents SDK for conversation UIs, and code agents (an agent loop the app runs on the AI gateway). Use when the app needs a conversational assistant or programmable AI." +--- + +# Base44 agents + +An agent is a `.jsonc` file under `base44/agents/`; writing it defines the agent. Build the conversation UI with the agents SDK, or run a code agent on the AI gateway. + +## Pick the reference for this app + +Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (over the Base44 MCP `execute_api` +tool, or with your Base44 CLI token); it returns the framework, app type, runtime, +entitlements and the exact reference files to read. Without it, detect from the project files: +`src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a +native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. + +| Profile | Read | +|---|---| +| Web app, Vite SPA (the default web template) | [Agents (Vite SPA: tools are backend functions)](references/web-spa.md) | +| Web app, TanStack Start | [Agents (TanStack Start: tools are server routes)](references/web-tanstack.md) | diff --git a/skills/base44-platform-agents/references/web-spa.md b/skills/base44-platform-agents/references/web-spa.md new file mode 100644 index 0000000..b10011d --- /dev/null +++ b/skills/base44-platform-agents/references/web-spa.md @@ -0,0 +1,196 @@ + +# Base44 agents: Agents (Vite SPA: tools are backend functions) + + +You can create AI agents when the user asks for them. InvokeLLM is a lighter-weight LLM call; agents fit tasks needing entity access, complex workflows, and more. + +An agent is a JSON configuration file in the base44/agents/ directory; to delete an agent, delete its config file. + +``` +// base44/agents/task_manager.jsonc +{ + "description": "This is an agent that can edit and manage tasks in the app", + "instructions": "Given a user request, you will edit and manage tasks in the app", + "tool_configs": [] +} +``` + + + +Agents act as the current app user, but get no app data/function tools automatically. + +New agents start with `"tool_configs": []`. Add only the entity/backend-function permissions, plus app-user connector permissions, the agent actually needs, and tell the user which permissions you granted and why. When later adding a backend function, connector-link capability, or entity capability to an existing agent, change that agent's `tool_configs` only — leave unrelated agents untouched. + +App-user OAuth connector permission only exposes connector context and the connect-link tool — it does not grant API access and is not required for backend function tools. Request it when the agent should help app users connect or reconnect that account. + +tool_configs example: + +```json +[ + {"entity_name": "ToDo", "allowed_operations": ["create", "update", "delete", "read"]}, + {"entity_name": "Participant", "allowed_operations": ["read"]}, + {"function_name": "sendReminder", "description": "Send a reminder notification for a task"} +] +``` + +Entity entries take `entity_name` + `allowed_operations`; backend-function entries take `function_name` + `description`. Allowed operations: "create", "update", "delete", "read" only. Tool configs support entity tools and backend function tools only. Every agent has built-in web_search — no need to define it. + + + +Agents support WhatsApp and Telegram — add a channel only when explicitly requested. When you do, set its greeting in the agent's config file — `whatsapp_greeting` / `telegram_greeting` (the message shown when an app user first messages the agent on that channel) — and add the connect link in the UI. The links handle authentication automatically; Telegram requires a bot connected via the agent editor's Telegram tab first. + +```jsx +import { base44 } from "@/api/base44Client"; +WhatsApp +Telegram +``` + + + +Without an in-app UI, an agent is only usable from the dashboard/backoffice — so when you build an agent, also build the UI for its conversations in the app. + + +Manage conversations, add messages, and subscribe to updates: + +```js +import { base44 } from "@/api/base44Client"; + +const conversation = base44.agents.createConversation({ agent_name: "task_manager", metadata: { name: "My Conversation", description: "My Conversation" } }); +const conversations = base44.agents.listConversations({ agent_name: "task_manager" }); +const conversation = base44.agents.getConversation(conversationId); // conversation.messages = full history, for loading into the UI +base44.agents.updateConversation(conversationId, { metadata: { name: "...", description: "..." } }); +base44.agents.addMessage(conversation, { role: "user", content: "Hello, how are you?" }); // pass the entire conversation object +base44.agents.addMessage(conversation, { role: "user", content: "Hello", file_urls: ["...url1"] }); // upload files first, then pass URLs + +// Subscribe to updates — called on every streamed token; no API calls / heavy work in the callback +useEffect(() => { + const unsubscribe = base44.agents.subscribeToConversation(conversationId, (data) => { + setMessages(data.messages); + }); + return () => unsubscribe(); +}, [conversationId]); +``` + + + +Message bubble essentials — message: { role ('user' or assistant), content (render assistant content as markdown), tool_calls? }. Each toolCall: { name, status: 'pending'|'running'|'in_progress'|'completed'|'success'|'failed'|'error', arguments_string, results (JSON string or object — JSON.parse with fallback to raw), display_projection? }. Treat as failed when status is failed/error, or results match /error|failed/i, or parsedResults.success === false. display_projection: { hide_details, details_redacted, label, active_label, error_label } — when hide_details && details_redacted are both set, render only the state label (active_label while pending/running/in_progress, error_label on failure, label on success) and hide parameters/results. + + +```jsx +const FunctionDisplay = ({ toolCall }) => { + const [expanded, setExpanded] = useState(false); + // derive status icon/text per the status mapping above; honor display_projection hide mode + return ( +
+ + {expanded && ( +
+ {/* "Parameters:" — pretty-print JSON.parse(toolCall.arguments_string), fallback raw */} + {/* "Result:" — pretty-print parsedResults */} +
+ )} +
+ ); +}; + +export default function MessageBubble({ message }) { + const isUser = message.role === 'user'; + return ( +
+ {message.content && (isUser + ?

{message.content}

+ : {message.content})} + {message.tool_calls?.map((toolCall, idx) => )} +
+ ); +} +``` +
+ +
+ +In-app agents are managed and conversational: app users talk to them and the platform runs the agent loop for you. A code agent is the programmable counterpart — a backend function running an agent loop the app owns: your code decides what tools it has (any code you write), how it runs (state across steps, step limits, which model), and what happens with the result — it's backend work the app runs, not a conversation. Build one when AI is part of the app's machinery rather than a conversation surface. Don't simulate it by chaining InvokeLLM calls or pre-fetching everything into one call — that hard-codes the decisions the AI was supposed to make. It runs against the platform’s AI gateway — same models and credit quota, no API key — and is stateless between invocations: persistent memory means storing and replaying state (e.g. in an entity). Use model "automatic" unless the task needs a specific model — non-default models use more credits: only when needed, and tell the user. + +The gateway speaks OpenAI chat completions, so any agent SDK works against it. Example with the AI SDK — a background reviewer the app runs on a return request: + +```js +import { createClientFromRequest } from "npm:@base44/sdk@0.8.54"; +import { ToolLoopAgent, tool, stepCountIs, hasToolCall } from "npm:ai@7.0.16"; +import { createOpenAICompatible } from "npm:@ai-sdk/openai-compatible@3.0.5"; +import { z } from "npm:zod@4.4.3"; + +export default async function(req) { + const base44 = createClientFromRequest(req); + const user = await base44.auth.me(); + if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 }); + + const { return_id } = await req.json(); + const request = await base44.entities.ReturnRequest.get(return_id); + + const { baseURL, token, headers } = base44.asServiceRole.aiGateway.connection(); + const base44Models = createOpenAICompatible({ name: "base44", baseURL, apiKey: token, headers }); + + const agent = new ToolLoopAgent({ + model: base44Models("automatic"), + instructions: "Decide whether this return looks fine or needs the owner's attention. Check the customer's past orders as many times as you need, then submit your verdict.", + tools: { + searchOrders: tool({ + description: "This customer's past orders, optionally filtered by status", + inputSchema: z.object({ status: z.string().optional() }), + execute: ({ status }) => { + const query = { customer_email: request.customer_email }; + if (status) query.status = status; + return base44.entities.Order.filter(query, "-created_date", 50); + }, + }), + submitVerdict: tool({ + description: "Record the final verdict. Call this once, when you've decided.", + inputSchema: z.object({ decision: z.enum(["approved", "flagged"]), reason: z.string() }), + execute: ({ decision, reason }) => + base44.entities.ReturnRequest.update(return_id, { status: decision, review_note: reason }), + }), + }, + // stops at the first to trigger — submitVerdict was called, or 8 steps elapsed; + // mix whatever conditions fit the task (a step cap, a finish tool, a custom condition) + stopWhen: [stepCountIs(8), hasToolCall("submitVerdict")], + }); + + await agent.generate({ prompt: `Review this return request: ${JSON.stringify(request)}` }); + return Response.json({ ok: true }); +} +``` + +Images: when the agent needs to see an image, pass it as an image part in `messages`: + +```js +const { text } = await agent.generate({ messages: [{ role: "user", content: [ + { type: "text", text: `Review this receipt: ${JSON.stringify(receipt)}` }, + { type: "image", image: receipt.image_url }, +]}]}); +``` + +Always bound the loop — pick the stop strategy from the task's intent, giving it enough room to finish while still stopping a runaway; every step is another model call, so end the loop once the work is done. + +All other backend-function rules (deployment, secrets, error handling) apply as in the backend_functions guide. + + +Model values for base44.integrations.Core.InvokeLLM in the generated app: + +The SDK reference includes compatibility aliases. Use this catalog for current model IDs and capability constraints. + +Omit model (or use "automatic") unless the app needs an explicit model. Automatic selection supports text, uploaded files, and web search. + +| model | file_urls | add_context_from_internet | +|---|---|---| +| gemini_3_8_flash | yes | yes | +| gpt_6_luna | yes | no | +| claude-sonnet-5 | yes | no | +| claude_opus_5_5 | yes | no | +| gpt_6_sol | yes | no | +| gpt_6_astra | yes | no | +| claude_fable_5_1 | yes | no | +| glm_5_2 | no | no | + +An incompatible explicit model and capability combination raises an error. Non-default models use more integration credits; tell the user when choosing one. diff --git a/skills/base44-platform-agents/references/web-tanstack.md b/skills/base44-platform-agents/references/web-tanstack.md new file mode 100644 index 0000000..c763cc6 --- /dev/null +++ b/skills/base44-platform-agents/references/web-tanstack.md @@ -0,0 +1,185 @@ + +# Base44 agents: Agents (TanStack Start: tools are server routes) + + +You can create AI agents when the user asks for them. InvokeLLM is a lighter-weight LLM call; agents fit tasks needing entity access, complex workflows, and more. + +An agent is a JSON configuration file in the base44/agents/ directory; to delete an agent, delete its config file. + +``` +// base44/agents/task_manager.jsonc +{ + "description": "This is an agent that can edit and manage tasks in the app", + "instructions": "Given a user request, you will edit and manage tasks in the app", + "tool_configs": [] +} +``` + + + +Agents act as the current app user, but get no app data/server-route tools automatically. + +New agents start with `"tool_configs": []`. Add only the entity/server-route permissions, plus app-user connector permissions, the agent actually needs, and tell the user which permissions you granted and why. When later adding a server route, connector-link capability, or entity capability to an existing agent, change that agent's `tool_configs` only — leave unrelated agents untouched. + +App-user OAuth connector permission only exposes connector context and the connect-link tool — it does not grant API access and is not required for server route tools. Request it when the agent should help app users connect or reconnect that account. + +tool_configs example: + +```json +[ + {"entity_name": "ToDo", "allowed_operations": ["create", "update", "delete", "read"]}, + {"entity_name": "Participant", "allowed_operations": ["read"]}, + {"tool_name": "send_reminder", "route_path": "/api/agent/send-reminder", "method": "post", "description": "Send a reminder notification for a task. Body: {taskId: string}. Returns {sent: boolean}."} +] +``` + +Entity entries take `entity_name` + `allowed_operations`; server-route entries take `tool_name` (`[A-Za-z0-9_-]`, ≤64), `route_path`, `method` (lowercase: `get`, `post`, `put`, `patch` or `delete`; default `post`) + `description`. Allowed operations: "create", "update", "delete", "read" only. Tool configs support entity tools and server route tools only. Every agent has built-in web_search — no need to define it. + +A server-route tool calls one of this app's server routes (`src/routes/api/…`) as the chatting app user, so the route's `requireUserRoute` and `context.getBase44()` see that user: a `GET` route receives the tool's `query` as its query string; other methods also receive `body` as the JSON request body. Agent logic that needs server code gets its own route, written for the agent: JSON in, a compact JSON result out. The agent sees only `description`, so state when to call it, the exact input fields and what it returns. + + + +Agents support WhatsApp and Telegram — add a channel only when explicitly requested. When you do, set its greeting in the agent's config file — `whatsapp_greeting` / `telegram_greeting` (the message shown when an app user first messages the agent on that channel) — and add the connect link in the UI. The links handle authentication automatically; Telegram requires a bot connected via the agent editor's Telegram tab first. + +```jsx +import { base44 } from "@/api/base44Client"; +WhatsApp +Telegram +``` + + + +Without an in-app UI, an agent is only usable from the dashboard/backoffice — so when you build an agent, also build the UI for its conversations in the app. + + +Manage conversations, add messages, and subscribe to updates: + +```js +import { base44 } from "@/api/base44Client"; + +const conversation = base44.agents.createConversation({ agent_name: "task_manager", metadata: { name: "My Conversation", description: "My Conversation" } }); +const conversations = base44.agents.listConversations({ agent_name: "task_manager" }); +const conversation = base44.agents.getConversation(conversationId); // conversation.messages = full history, for loading into the UI +base44.agents.updateConversation(conversationId, { metadata: { name: "...", description: "..." } }); +base44.agents.addMessage(conversation, { role: "user", content: "Hello, how are you?" }); // pass the entire conversation object +base44.agents.addMessage(conversation, { role: "user", content: "Hello", file_urls: ["...url1"] }); // upload files first, then pass URLs + +// Subscribe to updates — called on every streamed token; no API calls / heavy work in the callback +useEffect(() => { + const unsubscribe = base44.agents.subscribeToConversation(conversationId, (data) => { + setMessages(data.messages); + }); + return () => unsubscribe(); +}, [conversationId]); +``` + + + +Message bubble essentials — message: { role ('user' or assistant), content (render assistant content as markdown), tool_calls? }. Each toolCall: { name, status: 'pending'|'running'|'in_progress'|'completed'|'success'|'failed'|'error', arguments_string, results (JSON string or object — JSON.parse with fallback to raw), display_projection? }. Treat as failed when status is failed/error, or results match /error|failed/i, or parsedResults.success === false. display_projection: { hide_details, details_redacted, label, active_label, error_label } — when hide_details && details_redacted are both set, render only the state label (active_label while pending/running/in_progress, error_label on failure, label on success) and hide parameters/results. + + +```jsx +const FunctionDisplay = ({ toolCall }) => { + const [expanded, setExpanded] = useState(false); + // derive status icon/text per the status mapping above; honor display_projection hide mode + return ( +
+ + {expanded && ( +
+ {/* "Parameters:" — pretty-print JSON.parse(toolCall.arguments_string), fallback raw */} + {/* "Result:" — pretty-print parsedResults */} +
+ )} +
+ ); +}; + +export default function MessageBubble({ message }) { + const isUser = message.role === 'user'; + return ( +
+ {message.content && (isUser + ?

{message.content}

+ : {message.content})} + {message.tool_calls?.map((toolCall, idx) => )} +
+ ); +} +``` +
+ +
+ +In-app agents are managed and conversational: app users talk to them and the platform runs the agent loop for you. A code agent is the programmable counterpart — an agent loop this app runs in its own server with TanStack AI (preinstalled): your code decides what tools it has (any server code), how many steps it takes, which model, and what happens with the result. Build one when AI is part of the app's machinery, or when the app needs its own AI chat with custom tools. Don't simulate it by chaining InvokeLLM calls or pre-fetching everything into one call — that hard-codes the decisions the AI was supposed to make. It runs against the platform’s AI gateway — same models and credit quota, no API key, streaming supported — and is stateless between requests: persistent memory means storing and replaying state (e.g. in an entity). Use model "automatic" unless the task needs a specific model — non-default models use more credits: only when needed, and tell the user. + +Build the adapter with `gatewayModel(base44, model)` from `src/lib/ai.server.js`, in server code only, passing `context.getBase44()`. It calls the gateway as the app (service role), since the gateway only accepts server-side service-role calls, so authenticate the caller first. Tools keep using `context.getBase44()` and act as the caller. Every call spends the app owner's credits, so require a signed-in caller. A streaming chat is a server function that returns the `chat(...)` stream, plus `useChat` in the page: + +```js +// src/lib/chat.js +import { chat, maxIterations, toolDefinition } from "@tanstack/ai"; +import { createServerFn } from "@tanstack/react-start"; +import { z } from "zod"; +import { gatewayModel } from "@/lib/ai.server"; +import { requireUser } from "@/lib/auth-middleware"; + +export const chatWithAssistant = createServerFn({ method: "POST" }) + .middleware([requireUser]) + .validator(z.object({ messages: z.array(z.any()).max(40) })) + .handler(({ data, context }) => { + const base44 = context.getBase44(); + const listOrders = toolDefinition({ + name: "listOrders", + description: "The caller's recent orders, optionally filtered by status", + inputSchema: z.object({ status: z.string().optional() }), + }).server(({ status }) => base44.entities.Order.filter(status ? { status } : {}, "-created_date", 20)); + + return chat({ + adapter: gatewayModel(base44), + messages: data.messages, + systemPrompts: ["You help customers with their orders."], + tools: [listOrders], + agentLoopStrategy: maxIterations(5), + }); + }); +``` + +```jsx +// the page (per-user, so the route sets ssr: false) +import { useChat } from "@tanstack/ai-react"; +import { chatWithAssistant } from "@/lib/chat"; + +const { messages, sendMessage, isLoading } = useChat({ + // send only the recent history: the server caps it, and a long chat would otherwise fail every turn + fetcher: ({ messages }, { signal }) => chatWithAssistant({ data: { messages: messages.slice(-40) }, signal }), +}); +// each message is { id, role, parts }: render part.content where part.type === "text" +``` + +Without a chat (a background reviewer, a classifier), run the loop in a server function guarded by `.middleware([requireUser])`: `await chat({ adapter, messages, tools, agentLoopStrategy, stream: false })` returns the final text, or pass `outputSchema` (a zod schema) instead of `stream: false` for typed JSON. + +Images: pass an image part in a message's `content`: `[{ type: "text", content: "Review this receipt" }, { type: "image", source: { type: "url", value: receipt.image_url } }]`. + +Always bound the loop with `agentLoopStrategy: maxIterations(n)` — pick n from the task's intent, giving it enough room to finish while still stopping a runaway; every step is another model call. + + +Model values for base44.integrations.Core.InvokeLLM in the generated app: + +The SDK reference includes compatibility aliases. Use this catalog for current model IDs and capability constraints. + +Omit model (or use "automatic") unless the app needs an explicit model. Automatic selection supports text, uploaded files, and web search. + +| model | file_urls | add_context_from_internet | +|---|---|---| +| gemini_3_8_flash | yes | yes | +| gpt_6_luna | yes | no | +| claude-sonnet-5 | yes | no | +| claude_opus_5_5 | yes | no | +| gpt_6_sol | yes | no | +| gpt_6_astra | yes | no | +| claude_fable_5_1 | yes | no | +| glm_5_2 | no | no | + +An incompatible explicit model and capability combination raises an error. Non-default models use more integration credits; tell the user when choosing one. diff --git a/skills/base44-platform-backend-functions/SKILL.md b/skills/base44-platform-backend-functions/SKILL.md new file mode 100644 index 0000000..cbf6c65 --- /dev/null +++ b/skills/base44-platform-backend-functions/SKILL.md @@ -0,0 +1,28 @@ +--- +name: base44-platform-backend-functions +description: "Base44 server-side code: backend functions (HTTP handlers in base44/functions//entry.ts on Cloudflare Workers) for Vite SPA apps, or TanStack Start server functions and server routes. Secrets, the Base44 SDK on the server, auth and service role, webhooks, admin-only handlers, frontend invocation, testing, and the InvokeLLM model table. Use when the app needs external APIs, privileged logic or scheduled work." +--- + +# Base44 backend functions + +A Vite SPA app runs server code as backend functions; a TanStack Start app runs it as server functions and server routes inside its own project. Writing the file deploys it. + +> **Requires the Builder plan or above.** The workspace's entitlement is reported by the agent-profile endpoint; on a lower plan the platform rejects the deploy. + +## Pick the reference for this app + +Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (over the Base44 MCP `execute_api` +tool, or with your Base44 CLI token); it returns the framework, app type, runtime, +entitlements and the exact reference files to read. Without it, detect from the project files: +`src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a +native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. + +| Profile | Read | +|---|---| +| Web app, Vite SPA (the default web template) | [Backend functions (Vite SPA)](references/web-spa.md) | +| Web app, TanStack Start | [Server functions and server routes (TanStack Start)](references/web-tanstack.md) | +| Native mobile app (Expo, React Native) | [Backend functions (mobile apps)](references/mobile.md) | +| Game, 2D (Phaser 4) | [Backend functions (Vite SPA)](references/web-spa.md) | +| Game, 3D (Three.js) | [Backend functions (Vite SPA)](references/web-spa.md) | diff --git a/skills/base44-platform-backend-functions/references/mobile.md b/skills/base44-platform-backend-functions/references/mobile.md new file mode 100644 index 0000000..84318d4 --- /dev/null +++ b/skills/base44-platform-backend-functions/references/mobile.md @@ -0,0 +1,371 @@ + +# Base44 backend functions: Backend functions (mobile apps) + +When a user asks you to integrate with an external API that has no built-in integration, you can write custom backend functions to integrate with the API. +The backend functions are basically an HTTP handler that you can define in `base44/functions/{functionName}/entry.ts`. +It has to export a fetch function that will be called with the incoming request. Logic needed by more than one function must live in a `base44/shared/` module they both import — extract it, never copy it between functions. + +For external webhooks, browser GET links and OAuth callbacks, use the server-set `X-Base44-App-Url` origin plus `/functions/`. Never expose dispatcher `/run/` or deployment URLs; the dispatcher requires platform-only authentication. Preserve callback query parameters and validate OAuth state in the handler. + + +**Validation Flow**: After you generate the function code, it automatically goes through lint validation before deployment. + +**What gets REJECTED** (deployment fails): +- Syntax errors, missing imports, unmatched brackets/braces +- Wrong function structure or return types + +**What PASSES** (deployment succeeds): +- Unused variables, `let` instead of `const`, unused catch parameters +- These "style" issues are ignored to prevent deployment loops + + +- must use `export default async function(req) { ... }` handler - no other patterns accepted +- must declare variables before using them (use `const`, `let`, or `var`) +- must not redeclare variables already in scope (e.g. `user`, `req`) +- must prefix **external-dependency** imports with `npm:package@version` (relative imports of your own files are not external deps) +- must return Response objects - never return strings or plain objects directly +- must include proper try/catch error handling for robustness +- must validate user authentication with `base44.auth.me()` when using Base44 SDK +- must use proper JavaScript/TypeScript syntax with matched brackets and braces +- must validate all regex patterns are syntactically correct before writing +- must not throw or `await` at module top-level — keep all init inside the handler (top-level failures cause boot errors, not 500s) +- must `await` every Base44 SDK call (`base44.entities.X.list()`, `base44.auth.me()`, `base44.functions.invoke(...)` all return Promises) +- **Shared modules** - never duplicate logic across functions; extract it to a `base44/shared/` module, e.g. `import { calc } from "../../shared/calc.ts"`; own-dir helpers import as `./util.ts`. +- **Never cross the boundary** - `base44/` is server-side and never in the client bundle: never import it from a frontend file (the screen calls `base44.functions.invoke(...)` instead), and never import `src/` or `@/` from a function. + + + + + +1. CRITICAL: before writing any function, if you need to use a secret / api key, make sure to check if it's already set. If it is missing, ask the user to add it in the app dashboard (Settings → Environment variables). +2. ADMIN-ONLY FUNCTIONS: If a function should only be invoked by admin (e.g., scheduled tasks, functions a workflow or automation calls, admin dashboard operations, system maintenance), it MUST verify the authenticated user is an admin by checking `user.role === 'admin'` and return 403 Forbidden if not. +3. + +If the secrets you need are not set, ask the user to add them in the app dashboard (Settings → Environment variables) and wait for their confirmation before writing the function code. + +CRITICAL: a function that reads a secret / api key that is not set fails at runtime. + +For example, if the user asks for a WeatherAPI.com integration, you should request the WEATHER_API_KEY secret. + +**App Connectors Overview** +When a user asks for an integration to an external service, there is a capability called **app connectors** which allows users to connect to specific services using an OAuth flow with requested scopes. The supported connectors are defined in the specific app connectors coding instructions. + +**Integration Priority:** +When the user needs to integrate an external service, you MUST prioritize methods in the following order: +1. **App Connectors**: If the integration is supported by app connectors, you MUST prioritize it over all other methods. App connectors provide secure OAuth-based authentication with specific scopes. + +2. **Backend Functions with Secrets**: If app connectors are not available, use backend functions with API keys/secrets. + +**Important**: Always check if the requested integration is supported by app connectors first. If it is supported, use app connectors exclusively. Only fall back to other methods if the integration is not supported by app connectors. + +For example, if the user wants to integrate Google Calendar and it's supported by app connectors, always use the app connector OAuth integration. If app connectors support it but backend functions are not enabled, ask the user to enable them first, then proceed with the app connector authorization flow. + + + +**CRITICAL: Platform Version Differences** + +The way functions are called from the frontend depends on your app's platform version: + +- **Your app is Platform V3+**: Use `await base44.functions.invoke('functionName', params)` +- Import: `import { base44 } from "@/api/base44Client";` + +This affects how you reference the function in your examples and documentation. + + + + +THE ONLY WAY TO USE A FUNCTION in a page / component / layout is to use it from the base44 sdk. +the response of the function will be an axios response object. + +for example: +import { base44 } from "@/api/base44Client"; + +... +const response = await base44.functions.invoke('someFunction', {someParam: "someValue"}) +... + +The response is an Axios response object with structure: `{data, status, headers}` - your function's returned data is in `response.data`. + + + + +1. Prefer Web-standard APIs over external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws) +2. Import every external dependency with an `npm:` specifier, e.g. `import Stripe from "npm:stripe@18.5.0"`. +3. Pin a version on every external import. For the Base44 SDK use exactly `npm:@base44/sdk@0.8.54`. +4. Already have an `esm.sh` or `unpkg.com` URL? Replace the CDN hostname with the `npm:` specifier. +5. Export a default request handler: `export default async function(req) { ... }`. +6. Secrets: `import { secrets } from "base44:runtime"` → `secrets.get("NAME")`; pre-populated: BASE44_APP_ID. Set other secrets in dashboard environment variables. +7. NEVER ask users for BASE44_SERVICE_TOKEN, BASE44_SERVICE_ROLE_KEY, or similar - these secrets don't exist. Use `createClientFromRequest(req)` for auth, and `base44.asServiceRole` for service role level operations. +8. Naming: USE ONLY camelCase for function names. No spaces, no /, no hyphens. + **REMEMBER**: Frontend usage differs by platform version - see platform_version_note above. +9. Use `base44:runtime` for secrets (`secrets.get`) and post-response work (`waitUntil`). For work to complete after sending a response: `import { waitUntil } from "base44:runtime"; waitUntil(myAsyncOp())`. + +10. Write files using javascript. No need to use typescript. +11. Don't use multiple paths, only / is allowed when making an API call. +12. Parameters can only be passed through the payload. URL parameters are not allowed and won't work. +13. CRITICAL: For stripe webhooks make sure to do base44 auth after setting the token from request.headers before ANY stripe signature validation. +14. WEB CRYPTO IS ASYNC: The runtime uses the Web Crypto API (SubtleCrypto) which is asynchronous, unlike Node.js's synchronous crypto. For Stripe webhook signature verification, you MUST use `await stripe.webhooks.constructEventAsync(body, signature, secret)` instead of `constructEvent()`. The synchronous version will throw "SubtleCryptoProvider cannot be used in a synchronous context". +15. +16. Base44 SDK usage (service role + request-based init): + 16.a Always import the SDK as: `import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54';` + 16.b Create the client from the incoming request: + +import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54'; + +export default async function(req) { + try { + const base44 = createClientFromRequest(req); + const user = await base44.auth.me(); + + if (!user) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // User-scoped operations (use app-user token) + const items = await base44.entities.SampleData.list(); + + // Service-scoped operations (admin privileges) + const allItems = await base44.asServiceRole.entities.SampleData.list(); + + return Response.json({ items, allItems }); + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }); + } +} + + + 16.c When to use service role vs user auth: + * Prefer user-scoped calls (`base44.entities/...`) when responding to frontend/user actions. + * Use service role (`base44.asServiceRole`) only when necessary for admin-level operations and AFTER verifying: + * There is an authenticated user (base44.auth.isAuthenticated() is true), and + * The business logic allows elevating privileges for this action. + * For webhooks or endpoints intended to be called without user authentication: + * Use the service role to interact with Base44. + * FIRST validate the request authenticity: + * If it's a webhook from a known provider, use its official signature validation or npm package. + * Otherwise, suggest adding a shared secret as a query param when registering the webhook and verify it on each call. + * With `base44.asServiceRole` you can use the following methods: + * `base44.asServiceRole.entities` - For working with entities as admin. + * `base44.asServiceRole.functions` - Call backend functions. + * `base44.asServiceRole.integrations` - Call integrations. + 16.d Calling other backend functions: + * You can invoke another backend function via the SDK: + +const res = await base44.functions.invoke('myFunction', { foo: 'bar' }); + +17. **SHARED CODE**: Never duplicate logic across functions — put it in a `base44/shared/` module and import it (`import { x } from "../../shared/x.ts"`); own-dir helpers import as `./util.ts`. +18. Never load full datasets into memory — paginate entity queries, stream large external responses. +19. **Fast requests**: requests are cut at 5min and a Worker holds 6 connections at once. Run independent calls in `Promise.allSettled` batches of ~6, never one await per item; one `filter({ id: { $in: ids } })` instead of a get per id; every outbound `fetch` takes `signal: AbortSignal.timeout(ms)` sized to the API (~10s, more for LLM/media); on 429 back off or fail, never a tight retry loop. +20. Do NOT use `eval()` or `new Function()`. +21. Do NOT instantiate WebAssembly from bytes constructed at runtime. + + + +**CRITICAL: Admin-Only Function Authentication** + +When creating backend functions that should ONLY be invoked by admin (e.g., scheduled tasks, functions a workflow or automation calls, admin dashboard operations, system maintenance), the function MUST: + +1. **Authenticate the user** using `base44.auth.me()` +2. **Verify the user is an admin** by checking `user.role === 'admin'` +3. **Return 403 Forbidden** if the user is not an admin + + +const base44 = createClientFromRequest(req); +const user = await base44.auth.me(); + +if (user?.role !== 'admin') { return Response.json({ error: 'Forbidden: Admin access required' }, { status: 403 }); } + +// Proceed with admin-only operations... + + +This prevents non-admin users from invoking sensitive operations by directly calling the function endpoint. + + + + + +Use backend functions only when the user asks you to integrate with an external API that has no built-in integration. +Always prefer to use the built-in integartions, as described in integrations, over using backend functions. + + + + + + +Please integrate OpenAI's gpt-4o-mini model to the app, using a backend function. +I want you to tell a joke to the user based on their name. + + + +Before writing such a function, I will need your help to set the OPENAI_API_KEY secret. + +[Assistant requests the OPENAI_API_KEY secret with description: "Please provide your OpenAI API key. You can get it from https://platform.openai.com/api-keys by creating an account and generating a new API key."] + + + +I have now set the OPENAI_API_KEY secret. +Please write the backend function. + + + +Sure, let me create the function for you. + +[Assistant creates base44/functions/openai/entry.ts with the following content:] + +import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54'; +import OpenAI from 'npm:openai'; +import { secrets } from 'base44:runtime'; + +export default async function(req) { + try { + const base44 = createClientFromRequest(req); + + const user = await base44.auth.me(); + if (!user) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // Construct SDK clients inside the handler — module-level init throws on missing + // secrets and crashes boot (no try/catch reached, no logs from app code). + const openai = new OpenAI({ apiKey: secrets.get("OPENAI_API_KEY") }); + + const response = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [ + { + role: "system", + content: "You are a witty comedian specializing in clean, clever jokes." + }, + { + role: "user", + content: 'write a joke about the user named ' + user.full_name + } + ] + }); + + return Response.json({ joke: response.choices[0].message.content }); + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }); + } +} + +I've created a backend function that uses OpenAI to generate personalized jokes. + + + + + + + +Please add the ability to export all tasks to a PDF file. + + + +I'll add a PDF export feature for your tasks. + +[Assistant creates base44/functions/exportTasks/entry.ts:] +import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54'; +import { jsPDF } from 'npm:jspdf@4.0.0'; + +export default async function(req) { + try { + const base44 = createClientFromRequest(req); + const user = await base44.auth.me(); + + if (!user) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + const tasks = await base44.entities.Task.filter({}); + const doc = new jsPDF(); + + // Title and headers + doc.setFontSize(20); + doc.text('Tasks Report', 20, 20); + + doc.setFontSize(10); + doc.text(`Generated: ${new Date().toLocaleDateString()}`, 20, 30); + + // Table headers + doc.setFontSize(12); + doc.text('Task', 20, 45); + doc.text('Status', 100, 45); + doc.text('Priority', 140, 45); + + // Task content + let y = 55; + tasks.forEach((task) => { + if (y > 270) { // New page if needed + doc.addPage(); + y = 20; + } + + doc.setFontSize(10); + doc.text(task.title, 20, y); + doc.text(task.status, 100, y); + doc.text(task.priority || 'normal', 140, y); + y += 10; + }); + + const pdfBytes = doc.output('arraybuffer'); + + return new Response(pdfBytes, { + status: 200, + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': 'attachment; filename=tasks.pdf' + } + }); + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }); + } +} + +[Assistant then edits the tasks screen: imports `base44` from `@/api/base44Client` and the `Download` icon, and adds an "Export PDF" button whose `handleExport` calls `base44.functions.invoke('exportTasks')`.] + +Added a PDF export button that generates a beautifully formatted tasks report. + + + + + +If the user wants more information about the function, test the input / output of the function, +use it from an API or as a webhook - +Tell them to go to dashboard -> code -> functions -> function_name. + +That's also where they'll find the url of the function endpoint. +(The frontend should only use the function from the SDK). + +If an in-app agent should use a backend function you created, edited, or reused, add it to that agent's `tool_configs`. + + + +A function deploys automatically once its file is written. Test it by invoking it — `base44.functions.invoke('myFunction', {param: "value"})` from the app, or its endpoint URL from dashboard → Code → Functions — and read its logs there (the `base44-troubleshooter` skill covers log access); logs catch logical errors even on success. + +**Typical workflow:** +1. Write the function code (automatically deploys) +2. Invoke it with a representative payload +3. Check the response and logs for issues +4. Fix the code if needed (automatically redeploys) +5. Re-test to confirm + + +Model values for base44.integrations.Core.InvokeLLM in the generated app: + +The SDK reference includes compatibility aliases. Use this catalog for current model IDs and capability constraints. + +Omit model (or use "automatic") unless the app needs an explicit model. Automatic selection supports text, uploaded files, and web search. + +| model | file_urls | add_context_from_internet | +|---|---|---| +| gemini_3_8_flash | yes | yes | +| gpt_6_luna | yes | no | +| claude-sonnet-5 | yes | no | +| claude_opus_5_5 | yes | no | +| gpt_6_sol | yes | no | +| gpt_6_astra | yes | no | +| claude_fable_5_1 | yes | no | +| glm_5_2 | no | no | + +An incompatible explicit model and capability combination raises an error. Non-default models use more integration credits; tell the user when choosing one. diff --git a/skills/base44-platform-backend-functions/references/web-spa.md b/skills/base44-platform-backend-functions/references/web-spa.md new file mode 100644 index 0000000..06fe1fa --- /dev/null +++ b/skills/base44-platform-backend-functions/references/web-spa.md @@ -0,0 +1,206 @@ + +# Base44 backend functions: Backend functions (Vite SPA) + + +Backend functions: HTTP handlers in `base44/functions/{functionName}/entry.ts`, for external APIs with no built-in integration. Always prefer built-in integrations when they cover the need. Logic needed by more than one function must live in a `base44/shared/` module they both import — extract it, never copy it between functions. + + + +For external-service integrations, in order: +1. App connectors — when a connector supports the service, use it exclusively (e.g. Google Calendar via its connector). + +2. Backend functions with secrets — when connectors don't cover the service, API keys/secrets. + + + + +Before writing a function that needs a secret/API key, check it's set — using an unset secret fails. If missing, ask the user to add it in the app dashboard (Settings → Environment variables) and wait for their confirmation before writing code that reads it. +Secrets: `import { secrets } from "base44:runtime"` → `secrets.get("NAME")`; pre-populated: BASE44_APP_ID. Set other secrets in dashboard settings - environment variables. +Never ask for BASE44_SERVICE_TOKEN, BASE44_SERVICE_ROLE_KEY, or similar — those secrets don't exist: auth comes from `createClientFromRequest(req)`, service-role from `base44.asServiceRole`. + + + +Code passes lint validation before deployment. +Rejected: syntax errors, missing imports, unmatched brackets/braces, wrong function structure or return types. +Accepted (to prevent deployment loops): unused variables, `let` vs `const`, unused catch params. + +Requirements: +- Handler: `export default async function(req: Request): Promise { ... }` — the only accepted pattern. +- Return Response objects, not strings/plain objects. +- All initialization inside the handler — module-top-level `throw`/`await` = boot error instead of a 500: no try/catch reached, no app logs. +- try/catch the handler body; return a proper error response. +- `await` every Base44 SDK call — `base44.entities.X.list()`, `base44.auth.me()`, `base44.functions.invoke(...)` all return Promises. +- Validate user auth with `base44.auth.me()` when using the SDK. +- Declare variables before use; don't redeclare in-scope names (`user`, `req`); matched brackets; syntactically valid regex. +- Never duplicate logic across functions — extract it to a `base44/shared/` module and import it (`import { calc } from "../../shared/calc.ts"`); own-dir helpers import as `./util.ts`. +- `base44/` is server-side and never in the client bundle: never import it from a frontend file (the page calls `base44.functions.invoke(...)` instead), and never import `src/` or `@/` from a function. + + + +For external webhooks, browser GET links and OAuth callbacks, use the server-set `X-Base44-App-Url` origin plus `/functions/`. Never expose dispatcher `/run/` or deployment URLs; the dispatcher requires platform-only authentication. Preserve callback query parameters and validate OAuth state in the handler. +- Web-standard APIs over external deps (fetch not Axios, WebSockets API not node-ws). +- External deps: import with a versioned `npm:` specifier (Base44 SDK: `npm:@base44/sdk@0.8.54`); rewrite `esm.sh` / `unpkg.com` URLs to `npm:`. +- JavaScript, not TypeScript. Names camelCase only: no spaces, `/`, hyphens. +- Use `base44:runtime` for secrets and post-response work (see below); no `eval()`/`new Function()`; no WebAssembly from runtime-built bytes. +- Paginate entity queries, stream large responses — don't load full datasets into memory. +- Requests are cut at 5min and a Worker holds 6 connections at once: run independent calls in `Promise.allSettled` batches of ~6, never one await per item; one `filter({ id: { $in: ids } })` instead of a get per id; every outbound `fetch` takes `signal: AbortSignal.timeout(ms)` sized to the API (~10s, more for LLM/media); on 429 back off or fail, never a tight retry loop. + + + +For work to complete after sending a response (analytics pings, non-critical logging): import `waitUntil` from `base44:runtime` and call `waitUntil(myAsyncOp())`. The function returns the response immediately; the Worker stays alive until the promise settles. +```js +import { waitUntil } from "base44:runtime"; +export default async function(req) { + waitUntil(fetch("https://hooks.example.com/ping", { method: "POST" })); + return Response.json({ ok: true }); +} +``` + + + + +```js +import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54'; + +export default async function(req) { + try { + const base44 = createClientFromRequest(req); + const user = await base44.auth.me(); + if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 }); + const { items } = await base44.entities.SampleData.filter({ status: 'open' }, { sort: '-created_date', limit: 50 }); // user-scoped (app-user token), one page + const total = await base44.asServiceRole.entities.SampleData.count(); // service-scoped (admin), a number: no records read + const res = await base44.functions.invoke('myFunction', { foo: 'bar' }); // another backend function + return Response.json({ items, total }); + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }); + } +} +``` + +Prefer user-scoped calls (`base44.entities...`) for frontend/user actions. `base44.asServiceRole` (exposes `entities`, `functions`, `integrations`) only when admin-level operations are necessary, after verifying `base44.auth.isAuthenticated()` is true and the business logic allows the elevation. One exception: a deliberately public lookup, such as a visitor checking their own submission, may skip the sign-in check when a long random code (`crypto.randomUUID()`) saved at submission is the check; reject a missing or blank code, match it exactly, and return only the fields needed. + + + +Endpoints called without user auth (webhooks): use the service role; validate request authenticity first — anyone can reach the endpoint. Known providers: official signature validation/npm package; otherwise a shared secret as a query param, verified each call. +Stripe: do the base44 auth — after setting the token from request.headers — before any Stripe signature validation. Web Crypto is async (SubtleCrypto): `await stripe.webhooks.constructEventAsync(body, signature, secret)`; synchronous `constructEvent()` throws "SubtleCryptoProvider cannot be used in a synchronous context". + + + +Admin-only functions (scheduled tasks, functions a workflow or automation calls, admin dashboard ops, maintenance) MUST authenticate via `base44.auth.me()`, verify `user.role === 'admin'`, and return 403 Forbidden otherwise — this stops non-admins from invoking the endpoint directly. + + + + +Platform V3+: pages/components/layouts use functions only through the SDK: + +```js +import { base44 } from "@/api/base44Client"; +const response = await base44.functions.invoke('someFunction', {someParam: "someValue"}) +``` + +Response: Axios response object `{data, status, headers}` — your function's data is in `response.data`. + +Parameters only through the payload — URL parameters won't work. Single path only (`/`, no multiple paths). + + + +Function details, input/output testing, API/webhook usage, endpoint URL: dashboard -> code -> functions -> function_name. + + + +A function deploys automatically once its file is written. Test it by invoking it — `base44.functions.invoke('myFunction', {param: "value"})` from the app, or its endpoint URL from dashboard → Code → Functions — and read its logs there (the `base44-troubleshooter` skill covers log access); logs catch logical errors even on success. Write (auto-deploys) → invoke → check response/logs → fix (auto-redeploys) → re-test. + + + + +Integrate OpenAI's gpt-4o-mini via a backend function: tell the user a joke based on their name. + + +I need the OPENAI_API_KEY secret first — please add it in the app dashboard (Settings → Environment variables). + + +I set the OPENAI_API_KEY secret. Please write the function. + + +``` +// base44/functions/openai/entry.ts +import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54'; +import OpenAI from 'npm:openai'; +import { secrets } from 'base44:runtime'; + +export default async function(req) { + try { + const base44 = createClientFromRequest(req); + const user = await base44.auth.me(); + if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 }); + // SDK clients constructed inside the handler — module-level init throws on missing secrets and crashes boot. + const openai = new OpenAI({ apiKey: secrets.get("OPENAI_API_KEY") }); + const response = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: 'write a clean, clever joke about the user named ' + user.full_name }] + }); + return Response.json({ joke: response.choices[0].message.content }); + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }); + } +} + +``` + +Created a backend function that generates personalized jokes. + + + + + +Add the ability to export all tasks to a PDF file. + + +``` +// base44/functions/exportTasks/entry.ts +import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54'; +import { jsPDF } from 'npm:jspdf@4.0.0'; + +export default async function(req) { + try { + // ...createClientFromRequest(req) + auth.me() + 401 guard, as in base44_sdk_usage... + const tasks = await base44.entities.Task.filter({}); + const doc = new jsPDF(); + // ...doc.text title/headers + one row per task; doc.addPage() when y > 270... + const pdfBytes = doc.output('arraybuffer'); + return new Response(pdfBytes, { + status: 200, + headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': 'attachment; filename=tasks.pdf' } + }); + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }); + } +} +``` + +Then in `src/pages/Tasks.jsx`: import `base44` from `@/api/base44Client` and the `Download` icon, and add an "Export PDF" button whose `handleExport` calls `base44.functions.invoke('exportTasks')` and downloads the response blob. + +Added a PDF export button. + + + +Model values for base44.integrations.Core.InvokeLLM in the generated app: + +The SDK reference includes compatibility aliases. Use this catalog for current model IDs and capability constraints. + +Omit model (or use "automatic") unless the app needs an explicit model. Automatic selection supports text, uploaded files, and web search. + +| model | file_urls | add_context_from_internet | +|---|---|---| +| gemini_3_8_flash | yes | yes | +| gpt_6_luna | yes | no | +| claude-sonnet-5 | yes | no | +| claude_opus_5_5 | yes | no | +| gpt_6_sol | yes | no | +| gpt_6_astra | yes | no | +| claude_fable_5_1 | yes | no | +| glm_5_2 | no | no | + +An incompatible explicit model and capability combination raises an error. Non-default models use more integration credits; tell the user when choosing one. diff --git a/skills/base44-platform-backend-functions/references/web-tanstack.md b/skills/base44-platform-backend-functions/references/web-tanstack.md new file mode 100644 index 0000000..b48b874 --- /dev/null +++ b/skills/base44-platform-backend-functions/references/web-tanstack.md @@ -0,0 +1,32 @@ + +# Base44 backend functions: Server functions and server routes (TanStack Start) + + +All server-side code is a TanStack Start server function or server route under `src/`, run by Base44 on Cloudflare Workers. Prefer built-in integrations and connectors when they cover the need. Core integrations that read a user's files or spend credits (`InvokeLLM`, `ExtractDataFromUploadedFile`, transcription, image generation) run in a server function: upload from the browser, then pass the `file_url`/`file_uri` to it — never parse PDFs, images or spreadsheets in the browser. + + + +- Server function — `createServerFn({ method })` in `src/lib/server-fns.js` (or another module a route imports; server-only helpers go in `*.server.js` files, kept out of the browser bundle). The default for anything a page needs from the server: loader data, writes, calls that need a secret. An RPC called from a `loader` (`loader: () => listOrders()`) or a component (`useServerFn(createOrder)` + a TanStack Query mutation); `GET` reads, `POST` writes; validate with zod `.validator(z.object({ … }))` and call as `fn({ data: { … } })`: only `data` reaches the handler, so pass one object (an update is `fn({ data: { id, ...patch } })`), never a bare id. +- Server route — `src/routes/api/.js`: `createFileRoute("/api/")({ server: { handlers: { GET: async ({ request, params, context }) => Response.json(...) } } })`. A raw HTTP endpoint, only for a URL something other than this app's own code requests: webhooks and callbacks, a link or image the browser loads by itself (``, ``), feeds and public APIs, tools an in-app agent or the app's MCP server calls. The app's own code never fetches its own routes — streamed AI replies and third-party calls are server functions. Never define a route under `/api/apps`, `/api/app-logs` or `/ws-user-apps` — shadowing the Base44 platform API breaks the app's data layer. +- An AI chat or agent loop (streamed replies, the model calling tools) is a server function running TanStack AI on the app's AI gateway, with no API key: see the `base44-platform-agents` skill for the pattern before writing one, rather than streaming InvokeLLM output by hand. +- Logic two handlers share lives in a plain module both import — never one calling the other over HTTP. + + + +Every server function and server route handler receives `context.getBase44()` (wired in `src/start.js`): a Base44 SDK client acting as the visitor — anonymous when nobody is signed in — so entity access rules apply as in the browser. `context.getBase44().asServiceRole` bypasses them: use it only when the handler must see more than its caller, and never return more than the caller should see. Entities, users, connectors (`getConnection`) and `integrations.Core.*` use the browser SDK's API; auth comes from the platform headers, so nothing needs configuring. Call `getBase44()` only in handlers that need Base44 — a handler that never calls it must still answer without those headers. +A failed SDK call rejects with an error carrying the HTTP `status`; `entities..get(id)` rejects with 404 when the record is missing or hidden by the caller's access rules. When a route should show not-found, return null from the server function for that 404 and `throw notFound()` in the loader; rethrow every other error. + + + +Server functions and server routes are public endpoints: anyone can call them, with any input. Before `asServiceRole` or credit-spending work (Core integrations such as InvokeLLM), verify the caller and that the business logic allows it: `.middleware([requireUser])` from `@/lib/auth-middleware` (`createServerFn({ method: "POST" }).middleware([requireUser]).handler(({ context }) => …)`) rejects anonymous callers with 401 and sets `context.user`; a server route whose callers send a Base44 token uses `server: { middleware: [requireUserRoute], handlers }` from the same module, the same way (only on `src/routes/api/*` files — a route's middleware also runs for every route under it). Links and images the browser loads by itself carry no token, so a route serving them is public or checks a short-lived signed token a server function issued. Admin-only handlers also require `context.user.role === "admin"` and reject anyone else with 403. Skip the sign-in check only for a deliberately public feature (a no-login app whose visitors are meant to trigger it), and bound its input: `.max()` on every string and array, `z.enum` for choices. Webhooks verify the request's signature or shared secret instead. + + + +Before writing server code that needs an API key or secret, check it is set — an unset secret fails. If missing, ask the user to add it in the app dashboard (Settings → Environment variables) and wait for their confirmation before writing code that reads it. Read it with `import { secrets } from "base44:runtime"` → `secrets.get("NAME")` (`undefined` if unset) only in server functions, server routes and `*.server.js` files; never `process.env` or a `VITE_*` var (build-time, client-visible). A set or rotated secret reaches the dev preview and the latest commit's worker within seconds — no redeploy; a published app behind the latest commit picks it up on its next publish. + + + +Server code runs on Cloudflare Workers with `nodejs_compat`: most Node built-ins work, but there is no filesystem, no state shared between requests (module-level caches reset at any time), and no timer that outlives a response — `waitUntil` from `base44:runtime` keeps post-response work alive. Web-standard APIs over dependencies (`fetch`, not axios). `console.log` from server code is not shown to the user anywhere: while debugging, return the diagnostic in the response. + diff --git a/skills/base44-platform-connectors/SKILL.md b/skills/base44-platform-connectors/SKILL.md new file mode 100644 index 0000000..55ee847 --- /dev/null +++ b/skills/base44-platform-connectors/SKILL.md @@ -0,0 +1,26 @@ +--- +name: base44-platform-connectors +description: "Base44 app connectors: OAuth integrations with external services (Google, Slack, GitHub and more) in shared, app-user and BYO-shared modes, how to connect one from an external agent, and how server code reads the token with getConnection. Use when the app must act on a user's or the builder's account at a third-party service." +--- + +# Base44 connectors + +Connectors are configured against the app, not authored as files: list the catalog, connect with the complete scope set, then read tokens in server code. + +> **Requires the Builder plan or above.** The workspace's entitlement is reported by the agent-profile endpoint; on a lower plan the platform rejects the deploy. + +## Pick the reference for this app + +Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (over the Base44 MCP `execute_api` +tool, or with your Base44 CLI token); it returns the framework, app type, runtime, +entitlements and the exact reference files to read. Without it, detect from the project files: +`src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a +native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. + +| Profile | Read | +|---|---| +| Web app, Vite SPA (the default web template) | [App connectors (Vite SPA)](references/web-spa.md) | +| Web app, TanStack Start | [App connectors (TanStack Start)](references/web-tanstack.md) | +| Native mobile app (Expo, React Native) | [App connectors (mobile apps)](references/mobile.md) | diff --git a/skills/base44-platform-connectors/references/mobile.md b/skills/base44-platform-connectors/references/mobile.md new file mode 100644 index 0000000..9a115cb --- /dev/null +++ b/skills/base44-platform-connectors/references/mobile.md @@ -0,0 +1,37 @@ + +# Base44 connectors: App connectors (mobile apps) + + +App connectors allow you to integrate with external services in an easy way using OAuth. +App connectors come in the following types: + + +1. **Shared connector**: The app builder connects their own account. All app users share the app builder's single connection. Backend functions call the external API on the app builder's behalf. See the connector's details (`list_connectors`). +2. **App user connector**: The app builder provides their OAuth app credentials (client ID, client secret). Each App User connects their own account individually. Backend functions call the external API on the app user's behalf using the app user's tokens. See the connector's details (`list_connectors`). +3. **BYO shared connector**: A workspace admin registers their own OAuth app (an `OrganizationConnector`). The app builder consents once against it; the resulting token is shared across all app users. Use when the platform doesn't have its own OAuth app for the integration (e.g., Snowflake, Databricks) or when the workspace wants to use its own OAuth app. See the connector's details (`list_connectors`). + + +Backend functions MUST be enabled for each connector type. + + +Connecting a connector happens outside the code: read the catalog and each connector's details (description, scopes, auth status, connection mode) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available --app-id `; connect or re-scope a shared connector with `initiate_connector_connection` / `base44 connectors initiate` passing the complete scope set (it replaces, never merges), and let the user finish OAuth in their browser. App-user and BYO-shared connectors are registered by a workspace admin in Settings → OAuth Connectors. + + + +The catalog is live: list it with `list_connectors` (MCP) or `base44 connectors list-available`; each entry gives the display name and its `integration_type`. + + +Use the connector's integration_type for getConnection. +Many connectors also support webhooks for real-time event notifications. Once a connector is authorized, you can create a workflow with a connector trigger to subscribe to events (see the `base44-platform-workflows` skill). + + +App connectors require a Builder+ subscription; the app profile reports the workspace's entitlement. + +When the user asks to connect or use an external service: +1. If the catalog has no connector for the service, do not use the connector APIs. Integrate with backend functions with secrets or direct API calls instead. +2. Read the connector's details (description, scopes, auth status, connection mode, usage guide) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available`, and follow its connection mode: shared (one account serves every app user) or app user (each user connects their own). +3. Write connector-backed code only once the connector is authorized; in server code read its token with `base44.asServiceRole.connectors.getConnection(integration_type)` and make the API calls yourself. + + diff --git a/skills/base44-platform-connectors/references/web-spa.md b/skills/base44-platform-connectors/references/web-spa.md new file mode 100644 index 0000000..72b775d --- /dev/null +++ b/skills/base44-platform-connectors/references/web-spa.md @@ -0,0 +1,34 @@ + +# Base44 connectors: App connectors (Vite SPA) + + +App connectors integrate the app with external services via OAuth. + + +1. Shared: builder connects their own account; all app users share it; backend functions act on the builder's behalf. See . +2. App user: builder provides OAuth app credentials (client ID, client secret); each app user connects their own account; backend functions use that user's tokens. See . +3. BYO shared: workspace admin registers their own OAuth app (`OrganizationConnector`); builder consents once; token shared across all app users. For services with no platform OAuth app (e.g., Snowflake, Databricks) or when the workspace wants its own. See . + + + +Connecting a connector happens outside the code: read the catalog and each connector's details (description, scopes, auth status, connection mode) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available --app-id `; connect or re-scope a shared connector with `initiate_connector_connection` / `base44 connectors initiate` passing the complete scope set (it replaces, never merges), and let the user finish OAuth in their browser. App-user and BYO-shared connectors are registered by a workspace admin in Settings → OAuth Connectors. + + + +The catalog is live: list it with `list_connectors` (MCP) or `base44 connectors list-available`; each entry gives the display name and its `integration_type`. + + +Use the connector's integration_type for getConnection. +Many connectors support webhooks: once authorized, create a workflow with a connector trigger (see the `base44-platform-workflows` skill) to route events to handler logic. + + +App connectors require a Builder+ subscription; the app profile reports the workspace's entitlement. + +When the user asks to connect or use an external service: +1. If the catalog has no connector for the service, do not use the connector APIs. Integrate with backend functions with secrets or direct API calls instead. +2. Read the connector's details (description, scopes, auth status, connection mode, usage guide) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available`, and follow its connection mode: shared (one account serves every app user) or app user (each user connects their own). +3. Write connector-backed code only once the connector is authorized; in server code read its token with `base44.asServiceRole.connectors.getConnection(integration_type)` and make the API calls yourself. + + diff --git a/skills/base44-platform-connectors/references/web-tanstack.md b/skills/base44-platform-connectors/references/web-tanstack.md new file mode 100644 index 0000000..8959f7e --- /dev/null +++ b/skills/base44-platform-connectors/references/web-tanstack.md @@ -0,0 +1,34 @@ + +# Base44 connectors: App connectors (TanStack Start) + + +App connectors integrate the app with external services via OAuth. + + +1. Shared: builder connects their own account; all app users share it; server code acts on the builder's behalf. See . +2. App user: builder provides OAuth app credentials (client ID, client secret); each app user connects their own account; server code uses that user's tokens. See . +3. BYO shared: workspace admin registers their own OAuth app (`OrganizationConnector`); builder consents once; token shared across all app users. For services with no platform OAuth app (e.g., Snowflake, Databricks) or when the workspace wants its own. See . + + + +Connecting a connector happens outside the code: read the catalog and each connector's details (description, scopes, auth status, connection mode) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available --app-id `; connect or re-scope a shared connector with `initiate_connector_connection` / `base44 connectors initiate` passing the complete scope set (it replaces, never merges), and let the user finish OAuth in their browser. App-user and BYO-shared connectors are registered by a workspace admin in Settings → OAuth Connectors. + + + +The catalog is live: list it with `list_connectors` (MCP) or `base44 connectors list-available`; each entry gives the display name and its `integration_type`. + + +Use the connector's integration_type for getConnection. +Many connectors support webhooks: once authorized, create a workflow with a connector trigger whose step calls `invoke_server_route` on a POST server route (see the `base44-platform-workflows` skill). + + +App connectors require a Builder+ subscription; the app profile reports the workspace's entitlement. + +When the user asks to connect or use an external service: +1. If the catalog has no connector for the service, do not use the connector APIs. Integrate with server code with secrets or direct API calls instead. +2. Read the connector's details (description, scopes, auth status, connection mode, usage guide) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available`, and follow its connection mode: shared (one account serves every app user) or app user (each user connects their own). +3. Write connector-backed code only once the connector is authorized; in server code read its token with `base44.asServiceRole.connectors.getConnection(integration_type)` and make the API calls yourself. + + diff --git a/skills/base44-platform-entities/SKILL.md b/skills/base44-platform-entities/SKILL.md new file mode 100644 index 0000000..bb161db --- /dev/null +++ b/skills/base44-platform-entities/SKILL.md @@ -0,0 +1,30 @@ +--- +name: base44-platform-entities +description: "Base44 entities: the JSON-schema data models under base44/entities/, the entity SDK, and row-level security (RLS) rules. Use when adding or changing stored data, queries, or per-user access rules in a Base44 app." +--- + +# Base44 entities + +Entities are JSON-schema objects stored as `base44/entities/.jsonc`; writing the file is what defines the data model. The platform applies each entity's `rls` rules on every app-user request. + +## Pick the reference for this app + +Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (over the Base44 MCP `execute_api` +tool, or with your Base44 CLI token); it returns the framework, app type, runtime, +entitlements and the exact reference files to read. Without it, detect from the project files: +`src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a +native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. + +## For every app + +- [Row-level security (RLS)](references/rls.md) + +| Profile | Read | +|---|---| +| Web app, Vite SPA (the default web template) | [Entities and the entity SDK (web apps)](references/web.md) | +| Web app, TanStack Start | [Entities and the entity SDK (web apps)](references/web.md) | +| Native mobile app (Expo, React Native) | [Entities and the entity SDK (mobile and game apps)](references/mobile-and-games.md) | +| Game, 2D (Phaser 4) | [Entities and the entity SDK (mobile and game apps)](references/mobile-and-games.md) | +| Game, 3D (Three.js) | [Entities and the entity SDK (mobile and game apps)](references/mobile-and-games.md) | diff --git a/skills/base44-platform-entities/references/mobile-and-games.md b/skills/base44-platform-entities/references/mobile-and-games.md new file mode 100644 index 0000000..27f8bd5 --- /dev/null +++ b/skills/base44-platform-entities/references/mobile-and-games.md @@ -0,0 +1,114 @@ + +# Base44 entities: Entities and the entity SDK (mobile and game apps) + +Entities are defined as json schema objects that define the shape of the data that is stored in the app. + + +IMPORTANT: +Every entity records has the built in attributes (no need to specify them in the schema): +- id +- created_date +- updated_date +- created_by_id (the ID of the user that created the record) + + + +Restoring entities is possible from the UI. + + +User entity is a built-in entity that exists on every app. +VERY IMPORTANT: YOU DON'T NEED TO CREATE THE USER ENTITY YOURSELF. it is created automatically and available for use on any app. + +Read-only attributes (you can not change them, you don't need to define them in the schema): +- id +- created_date +- full_name +- email + +Editable attributes: +- role (default values are 'admin' and 'user', but you can freely change, extend, or remove role values to fit the app's needs) + Example: changing roles to "manager", "employee", "viewer" or any other set of roles + +To customize role values or add additional attributes, edit `base44/entities/User.jsonc`. +Only include the fields you want to add or customize - built-in attributes (id, created_date, full_name, email) are included automatically. +Example: To change roles to 'manager', 'employee', 'viewer', include a "role" property with a custom enum. + +IMPORTANT: The User entity has special built-in security rules that only allow admin users to list, update, or delete other users. Regular users can only view and update their own user record. +Do not add custom User RLS unless the user explicitly asks to customize User access behavior. + + + +IMPORTANT: Never store large content (base64 images, PDFs, HTML blobs, binary data) directly on entity fields. Instead, upload files via UploadPrivateFile for anything an end user uploads and store only the returned file_uri. + + + +The Entities SDK can be imported from the pre-initialized base44 sdk client. + +use it like this: +import { base44 } from '@/api/base44Client'; +base44.entities.EntityName.list() will return the list of entities. + +e.g. if there's an entity named Todo, here's how to use the SDK: +import { base44 } from '@/api/base44Client'; +base44.entities.Todo.list('-updated_date', 20) will return the 20 most recently updated todos. +base44.entities.Todo.filter({status: 'active', created_by_id: user.id}, '-created_date', 10) will return the top 10 recently created active todos created by the current user. +base44.entities.Todo.create({title: "Todo 1", description: "Todo 1 description"}) will create a new todo. +base44.entities.Todo.bulkCreate([{title: "Todo 2", description: "abc"}, {title: "Todo 3", description: "123"}]) will create 2 new todos. +base44.entities.Todo.update(todo.id, {description: "new description"}) will update the todo with id 1. +base44.entities.Todo.updateMany({status: 'active'}, {$set: {status: 'done'}}) applies the same change to all matching todos (up to 500 records); returns has_more=true if remains; call again for the next batch — but your $set must change a field in the query; otherwise the same rows re-match forever. the 2nd arg uses MongoDB update operators (not a plain {field: value}): $set, $rename, $unset, $inc, $mul, $min, $max, $currentDate, $addToSet, $push, $pull. careful: no schema validation. +base44.entities.Todo.bulkUpdate([{id: id1, status: 'done'}, {id: id2, status: 'open'}]) will update many todos with different values in one call (up to 500). +base44.entities.Todo.delete(todo.id) will delete the todo with id 1. +base44.entities.Todo.deleteMany({status: 'archived'}) will delete all todos matching the query in one call. careful: a broad or empty ({}) query matches and deletes everything it's allowed to. + +in addition, you can use the built-in user entity through the SDK like this: +import { base44 } from '@/api/base44Client'; +await base44.entities.User.list() ... + +the authentication SDK provides special methods for the user entity: +await base44.auth.updateMe(data) - use this to update the current user's data. + +and if you want to access the current user, you can use the built-in user object: +const user = await base44.auth.me(); // can throw an error if the user is not logged in and the app is public. + +base44.auth.logout(redirectUrl?: string) - use this to logout the current user, redirects to the redirectUrl if provided, reloads the page otherwise. + +base44.auth.redirectToLogin(nextUrl?: string) - use this to login (this will redirect the user to the app's login page, +after successful login the user will be automatically redirected to the nextUrl if provided, +otherwise the user will be redirected to the page from which the login was triggered). + +to understand if the user is authenticated, you can use the base44.auth.isAuthenticated() which returns Promise. + +to save additional data on the current user, you can use the base44.auth.updateMe(data) +and data will be persisted (you can access it using await base44.auth.me()). +You can't override the built-in user attributes (id, email, full_name, role). + +all entity methods are pretty reliable, there's no need to try and catch errors. + +Real-time subscriptions - subscribe to entity changes in real-time: + +// Subscribe to ALL changes on an entity type (create, update, delete) +const unsubscribe = base44.entities.Todo.subscribe((event) => { + console.log(`Todo ${event.id} was ${event.type}d`); + console.log('Data:', event.data); +}); + +// Cleanup when done +unsubscribe(); + +// Example usage in a React component: +useEffect(() => { + const unsubscribe = base44.entities.Todo.subscribe((event) => { + if (event.type === 'create') { + setTodos(prev => [...prev, event.data]); + } else if (event.type === 'update') { + setTodos(prev => prev.map(t => t.id === event.id ? event.data : t)); + } else if (event.type === 'delete') { + setTodos(prev => prev.filter(t => t.id !== event.id)); + } + }); + + return unsubscribe; +}, []); + diff --git a/skills/base44-platform-entities/references/rls.md b/skills/base44-platform-entities/references/rls.md new file mode 100644 index 0000000..544c020 --- /dev/null +++ b/skills/base44-platform-entities/references/rls.md @@ -0,0 +1,144 @@ + +# Base44 entities: Row-level security (RLS) + + + + +Row-Level Security (RLS) decides which app users can read, create, update, or delete each record of an entity. You set it per entity in `base44/entities/.jsonc` under an `rls` key, and the platform applies it on every app-user request. + +Mental model: each operation gets a Mongo-style filter, and a user may perform that operation on the records that match the filter. An operation with no rule stays open to everyone — including anonymous visitors on public / no-login apps — which is right for genuinely shared data and wrong for anything personal. + + + +{ + "rls": { + "read": { ... }, + "create": { ... }, + "update": { ... }, + "delete": { ... } + } +} +- Leave an operation open by omitting its key (or using `{}` or `true`). +- Set the operations you want to protect. It's easy to lock reads and forget writes, so when the goal is "make it private," cover read, update, and delete together. `create` is usually fine left open — users make their own records, and the platform stamps the creator automatically. Two cases do want an explicit `create` rule: public / no-login apps (otherwise anonymous visitors can insert records), and scopes keyed on a field the creator sets — like `data.tenant_id` or `data.members` — so a user can't plant a record in someone else's scope (e.g. `"create": { "data.tenant_id": "{{user.data.tenant_id}}" }`). + + + +Records carry a few built-in fields at the top level — `id`, `created_by_id` (the creator's user id, your reliable "owner" handle), `created_by` (creator email), `created_date`, `updated_date`. Reference these directly. +Every field you defined on the entity lives under `data.`, so reference it with that prefix: a `status` field is `data.status`, a `members` field is `data.members`. + +Describe the current user with `{{...}}` templates: +- `{{user.id}}` — pair with `created_by_id` for ownership +- `{{user.email}}` +- `{{user.role}}` — "admin" or "user" +- `{{user.data.}}` — a custom field on the user, one level deep (e.g. `{{user.data.tenant_id}}`) +- When a user field is itself an array (e.g. `{{user.data.departments}}`), match a record field against the whole template as the `$in` operand: `{ "data.department": { "$in": "{{user.data.departments}}" } }` — the template is the entire value. Only this whole-operand form resolves; an array template used as a direct equality value (`{ "data.department": "{{user.data.departments}}" }`) or wrapped in a list (`{ "$in": ["{{user.data.departments}}"] }`) is left verbatim and matches nothing. + +Users can edit their own custom User fields (`base44.auth.updateMe`), so a rule may only trust a value the user can't set themselves: `{{user.id}}`, `{{user.email}}`, `{{user.role}}`, record fields the write rules protect (`created_by_id`, a `members` list), or a custom User field locked as in the_user_entity. An unlocked `{{user.data.}}` or custom field in `user_condition` lets a user set their own `tenant_id` to another tenant's and get that tenant's records. + +Gate on a user value when every relevant user actually has it set. If a user's field is empty the template resolves to null, which matches records whose field is also empty rather than blocking — so `created_by_id` (always present) is the most dependable anchor, and custom user fields work well once you make sure they're populated. + + + +To allow access based on who the user is rather than a field on the record, use `user_condition`, which compares user attributes for exact equality: + { "user_condition": { "role": "admin" } } +Keep `user_condition` as the only key in its object, and combine it with record conditions through `$or` / `$and` (see owner-or-admin below). A `$or` / `$and` / `$nor` is the same way — it must be the only key in its object, so to require a logical group *and* another condition, wrap both in an outer `$and` (e.g. `{ "$and": [ { "$or": [...] }, { "data.tenant_id": "{{user.data.tenant_id}}" } ] }`) rather than placing the field beside the `$or`. Use a plain value inside `user_condition` — it matches by exact equality. Role checks use "admin" (the elevated role) against the default "user". + + + +Reads run as a database query and accept the comparison and array operators — `$eq`/`$ne`, `$in`/`$nin`, `$gt`/`$gte`/`$lt`/`$lte`, `$exists`, `$all`, `$size`, `$elemMatch` — plus `$or`/`$and`/`$nor`. (Text search like `$regex` isn't available, so scope reads by exact field values.) Writes (create / update / delete) are checked against the single record in application code, which understands a focused, dependable set — build write rules from these and they behave exactly as written: + - equality: `{ "created_by_id": "{{user.id}}" }` + - `$in` / `$nin` with a list: `{ "data.status": { "$in": ["active", "trial"] } }` + - `$ne` with a single value: `{ "data.status": { "$ne": "archived" } }` + - `$or` / `$and`, alternating when you nest them (an `$or` holding `$and` branches, and vice-versa) + - `user_condition` + - top-level `data.` fields, i.e. a single-segment key like `data.tenant_id` + +Two natural adjustments for writes: + - Membership: on reads, `{ "data.members": "{{user.id}}" }` matches when the array contains the user. For writes, write the same idea as `{ "data.members": { "$in": ["{{user.id}}"] } }`. + - The read-only operators above (ranges, `$exists`, `$all`, `$size`, `$elemMatch`, and `$nor`) are great on `read`; keep write rules to the compact set above so they enforce cleanly. + +Handy detail: when `update`/`delete` are absent they inherit `write`, then `read`. So if an entity's protection currently lives on `read`, add matching explicit `update`/`delete` rules before you open `read` up, so writes stay covered. + + + +Match the pattern to what the data is; `created_by_id` is the owner throughout. + +Private to each user — their own records only (read / update / delete): + { "created_by_id": "{{user.id}}" } + +Owner, and admins see and manage everything: + { "$or": [ { "created_by_id": "{{user.id}}" }, { "user_condition": { "role": "admin" } } ] } + +Public to read, only the author edits — read open, writes owner-scoped: + "read": {}, + "create": { "created_by_id": "{{user.id}}" }, + "update": { "created_by_id": "{{user.id}}" }, + "delete": { "created_by_id": "{{user.id}}" } + +Everyone reads; only admins create or edit (announcements, shared catalog): + "read": {}, + "create": { "user_condition": { "role": "admin" } }, + "update": { "user_condition": { "role": "admin" } }, + "delete": { "user_condition": { "role": "admin" } } + +Visitor submissions the app's operator reviews (contact, booking, intake forms) — anyone submits; a logged-in submitter sees their own; only admins (plus any role that handles them) change them: + "create": {}, + "read": { "$or": [ { "created_by_id": "{{user.id}}" }, { "user_condition": { "role": "admin" } } ] }, + "update": { "user_condition": { "role": "admin" } }, + "delete": { "user_condition": { "role": "admin" } } + (If visitors who don't log in need to see their own submission later — a status page, a receipt — keep read closed. When they submit, save a long random code from a secure source (`crypto.randomUUID()`) in its own field and give it to them (not the record id or their email, which can be guessed). Look it up in a backend or server function with the service-role client (`asServiceRole`). It is a deliberately public endpoint: skip the sign-in check, since the code is the check. Reject a missing or blank code before the query, and match it exactly, never as a regex. Return only the fields they need.) + +Team / shared records — owner, a listed member, or an admin: + read: { "$or": [ { "created_by_id": "{{user.id}}" }, { "data.members": "{{user.id}}" }, { "user_condition": { "role": "admin" } } ] } + write: { "$or": [ { "created_by_id": "{{user.id}}" }, { "data.members": { "$in": ["{{user.id}}"] } }, { "user_condition": { "role": "admin" } } ] } + (If members only need to view, owner-or-admin on the write ops is simpler.) + This assumes `data.members` is a list of user IDs (plain strings). If it's a list of objects (e.g. `[{ "user_id": ... }]`), match the sub-field on reads — `{ "data.members.user_id": "{{user.id}}" }` — because a plain value compared to a list of objects matches nothing. For write access there's no clean form (the write gate can't reach into an object list), so anchor member writes on `created_by_id`/admin, or denormalize to a list of plain user-ID strings. + +Multi-tenant — each user sees only their organization's data (user carries `data.tenant_id`, so do the records). Apply to read/update/delete, and to create so a user can only add records in their own tenant, and lock the User's `tenant_id` (see the_user_entity): + { "data.tenant_id": "{{user.data.tenant_id}}" } + +Published-or-mine — a visibility state anyone can read, plus your own drafts: + read: { "$or": [ { "data.status": "published" }, { "created_by_id": "{{user.id}}" } ] } + create/update/delete: { "created_by_id": "{{user.id}}" } + (A plain `{ "data.status": "published" }` read is a fine visibility filter on its own; add the owner branch when people also need their own records. Combine visibility with identity using `$or`, or require both with `$and`.) + +One extra care on update: when access depends on a field the user could change (`data.tenant_id`, `data.members`), base the access decision on the unchangeable `created_by_id` rather than that field, so a user editing the field can't quietly grant or revoke their own access. A top-level `update` rule authorizes the *existing* record before the new values are applied — it gates who may edit the record, not which fields they may touch — so `created_by_id` is the dependable anchor here. + + + +A good rule is precise and lets the intended users through. Three quick checks keep it that way: +- Anchor on a field the app guarantees on every record. `created_by_id` is set automatically on every record, so an owner rule always lets owners in — it's the safest anchor. A custom field works as an anchor only when the app fills it on every record (for example a required field set at creation); gating on a field that's sometimes empty locks out users whose records don't have it. Live population isn't something you can check at build time, so prefer `created_by_id` unless you know the field is always set. +- Reference fields that exist on the entity's schema (with the `data.` prefix for custom ones). A rule pointing at a field the records don't have matches nothing. +- Read the rule back from the owner's and admin's perspective and confirm they still satisfy it. +- To close an operation entirely, point it at whoever should still have access — for example admin-only (`{ "user_condition": { "role": "admin" } }`). Keep the condition real and satisfiable: an impossible one like `{ "id": { "$exists": false } }` matches no record, so it shuts everyone out (owners included) rather than restricting a subset. +When a rule matches no one, reads come back empty and writes are refused — so this precision is what separates "secured" from "locked out." + + + +Read the entity's current `rls` first, then change only what needs changing: +- An operation already protected by a rule that fits the intent is done — keep it as-is. Rewording a working rule into a lateral or looser variant just adds churn; a change earns its place when it tightens access, fixes a rule that locks people out, or closes an operation that's still open. +- For an operation that is open but should be restricted, add a rule following the patterns above. +- For a whole-app request ("secure everything", "lock it down"), cover every entity that holds user data, not just the obvious ones — it's easy to secure most and leave one open. +- When a rule locks users out (see the checks above), repair it into the nearest correct rule that keeps the intended restriction, rather than dropping the protection. +- Respect deliberate choices. An operation the user wants public stays open. A `read` or `delete` set to `false` means "no one" on purpose, so leave it; on `create`/`update`, `false` isn't enforced, so treat those as open and give them a real rule if they should be restricted. A legacy top-level `write: false` behaves the same way — it denies `delete` but leaves `update` open — so replace it with explicit `update`/`delete` rules when writes should stay locked. +Change what differs from the current config, and leave the rest. + + + +The built-in `User` entity is governed by the platform's own permissions (for example, only admins manage other users), and a top-level `rls` on `User` isn't applied — so rely on those built-in permissions rather than adding one. For role-based access, put the `user_condition` role checks on the other entities' rules, where they take effect. + +Field-level rules on `User` properties do apply. A field lock is not the custom User RLS the entity guide tells you to skip, so add it even when the user didn't ask for User rules. Every custom User field another entity's rule trusts (`{{user.data.tenant_id}}`, `user_condition: { "data.department": ... }`) gets a write lock in `base44/entities/User.jsonc` — add its entry to the file's existing `properties`, keeping the fields already there: + "properties": { "tenant_id": { "type": "string", "rls": { "update": { "user_condition": { "collaborator_role": "editor" } } } } } +App users, app admins included, can then no longer change it, on first set too; the app's editors still can from the dashboard. Assign it in a backend or server function with the service-role client (`asServiceRole`, which skips these rules), after checking the user is entitled to it: an invite code, an admin's assignment, the tenant's verified domain. Backend functions need a paid plan; in an app with neither backend functions nor server functions, only the app's editors can set the field, from the dashboard, so if that won't scale use `created_by_id` or a `members` list instead. Never let a user pick their own tenant, company, or access level from the app's UI. A field that only holds the user's current choice among tenants (`active_tenant_id`) is not a boundary: the rule must also check membership in a list the user can't edit. + + + +1. Open `base44/entities/.jsonc`, note the owner field (`created_by_id`) and any custom field you'll reference. +2. Write one complete `rls` object covering each operation you're protecting, using the write-safe forms above and keeping any protection that should remain. +3. Save the file — the rules apply immediately to app-user requests. +4. Tell the user which operations you locked and to which rule, and confirm owners and admins keep the access they need. + + + diff --git a/skills/base44-platform-entities/references/web.md b/skills/base44-platform-entities/references/web.md new file mode 100644 index 0000000..9f2576a --- /dev/null +++ b/skills/base44-platform-entities/references/web.md @@ -0,0 +1,55 @@ + +# Base44 entities: Entities and the entity SDK (web apps) + +Entities = JSON schema objects defining stored data. Built-in attributes on every record (never declare): id, created_date, updated_date, created_by_id. + + +Restoring entities is possible from the UI. + + +User is built-in on every app — never create it. Read-only: id, created_date, full_name, email. Editable: role (defaults 'admin'/'user'; freely change values by editing base44/entities/User.jsonc, declaring only customized fields). Built-in security: only admins list/update/delete other users. Don't add custom User RLS unless explicitly asked. You cannot insert User records — users join via invites; say so if asked. + + + +Never store large content (base64, PDFs, blobs) in entity fields — upload via UploadPrivateFile for anything an end user uploads, store the returned file_uri; oversized fields break record operations. + + + +Entities SDK on the pre-initialized base44 client; each entity is base44.entities.EntityName: + +```js +import { base44 } from '@/api/base44Client'; + +base44.entities.Todo.list() +base44.entities.Todo.list('-updated_date', 20) // sort, limit +base44.entities.Todo.filter({status: 'active', created_by_id: user.id}, '-created_date', 10) +base44.entities.Todo.create({title: "Todo 1"}) +base44.entities.Todo.bulkCreate([{title: "Todo 2"}, {title: "Todo 3"}]) +base44.entities.Todo.update(todo.id, {description: "new"}) +base44.entities.Todo.updateMany({status: "active"}, {$set: {status: "done"}}) // same change to all matches; up to 500 records, returns has_more=true if remains — call again for the next batch, but your $set must change a field in the query or the same rows re-match forever. 2nd arg uses MongoDB update operators (not a plain {field: value}): $set, $rename, $unset, $inc, $mul, $min, $max, $currentDate, $addToSet, $push, $pull. careful: no schema validation +base44.entities.Todo.bulkUpdate([{id: id1, status: "done"}, {id: id2, status: "open"}]) // different change per record, up to 500 records +base44.entities.Todo.delete(todo.id) +base44.entities.Todo.deleteMany({status: "archived"}) // deletes all matches in one call. careful: a broad or empty ({}) query deletes everything it's allowed to +``` + + +Built-in User entity uses the same SDK for reads (await base44.entities.User.list(), …), but User records can't be created or imported (create returns 405) — invite users with `base44.users.inviteUser(email, role)`. Auth SDK: +- await base44.auth.me() — current user; can throw if not logged in on a public app. +- await base44.auth.updateMe(data) — persist extra data on the current user (returned by later me()); built-ins (id, email, full_name, role) can't be overridden. +- base44.auth.isAuthenticated() — Promise. +- base44.auth.logout(redirectUrl?: string) — then redirect to redirectUrl, or reload. +- base44.auth.redirectToLogin(nextUrl?: string) — app login page; after login → nextUrl, else the triggering page. + + + +```js +useEffect(() => { + // event: { id, type: 'create'|'update'|'delete', data } + const unsubscribe = base44.entities.Todo.subscribe((event) => { /* update state from event */ }); + return unsubscribe; +}, []); +``` + + diff --git a/skills/base44-platform-frontend/SKILL.md b/skills/base44-platform-frontend/SKILL.md new file mode 100644 index 0000000..1ab3092 --- /dev/null +++ b/skills/base44-platform-frontend/SKILL.md @@ -0,0 +1,26 @@ +--- +name: base44-platform-frontend +description: "Frontend coding standards for Base44 web apps: code conventions, the installed packages, routing (React Router in the Vite SPA, file routes in TanStack Start), Tailwind and design tokens, styling and componentization rules, npm installs, and how files are edited in the sandbox. Use when writing or changing pages, components or routes." +--- + +# Base44 frontend + +The builder's own frontend rules for Base44 web apps. Pick the reference for the app's framework; the TanStack reference ends with a worked example. + +## Pick the reference for this app + +Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (over the Base44 MCP `execute_api` +tool, or with your Base44 CLI token); it returns the framework, app type, runtime, +entitlements and the exact reference files to read. Without it, detect from the project files: +`src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a +native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. + +| Profile | Read | +|---|---| +| Web app, Vite SPA (the default web template) | [Web app, Vite SPA](references/web-spa.md) | +| Web app, TanStack Start | [Web app, TanStack Start](references/web-tanstack.md) | +| Native mobile app (Expo, React Native) | [Native mobile app (Expo, React Native, NativeWind)](references/mobile.md) | +| Game, 2D (Phaser 4) | [Web app, Vite SPA](references/web-spa.md) | +| Game, 3D (Three.js) | [Web app, Vite SPA](references/web-spa.md) | diff --git a/skills/base44-platform-frontend/references/mobile.md b/skills/base44-platform-frontend/references/mobile.md new file mode 100644 index 0000000..50e250f --- /dev/null +++ b/skills/base44-platform-frontend/references/mobile.md @@ -0,0 +1,138 @@ + +# Base44 frontend: Native mobile app (Expo, React Native, NativeWind) + +When coding, you are an exceptional software developer, expert in React Native and Expo mobile development. +You will develop screens, and when needed - you can develop components or edit the root layout of the app. + +1. CRITICAL: Think HOLISTICALLY and COMPREHENSIVELY BEFORE making an edit to a file. This means: +- Consider ALL relevant files in the project +- Review ALL previous file changes and user modifications +- Analyze the entire project context and dependencies +- Anticipate potential impacts on other parts of the system + + This holistic approach is ABSOLUTELY ESSENTIAL for creating coherent and effective applications. + +2. When writing a screen or a component, it has to be exported as default and have the same name. +3. When making a change to a screen or a component, consider all existing functionality. DO NOT override existing functionality, unless explicitly asked to do so by the user. + +4. Making edits to existing code: edit the existing file in place; rewrite a file whole only when most of it changes. + +For entities - ALWAYS PROVIDE THE FULL JSON SCHEMA. NO COMMENTS OR PLACEHOLDERS FOR PREVIOUS PROPERTIES. + + +This is a React Native (Expo) app — there is NO DOM. These rules are absolute: +- NEVER use HTML elements (`div`, `span`, `p`, `h1`, `img`, `button`, `input`, `a`, `table`, etc.). Use React Native core components: `View`, `Text`, `Pressable`, `TextInput`, `ScrollView`, `Image` (from expo-image), `Modal`, `Switch`, `ActivityIndicator`. +- ALL text must be wrapped in a `` component. Raw strings inside a `` crash the app. +- There is NO shadcn/ui and NO `@/components/ui` folder. Build UI from React Native primitives styled with NativeWind. +- NO `window`, `document`, `localStorage`, or other browser globals. For URL params use expo-router's `useLocalSearchParams()`. +- Use `@shopify/flash-list` (`FlashList`) for any scrollable list of repeated data; `ScrollView` only for short, fixed content. +- Use `Pressable` for tappable elements (not `TouchableOpacity` — `Pressable` is the modern API). +- For icons use `lucide-react-native` — include only icons you know exist in the library; invalid icon imports break the app. Icons take `size` and `color` props (NOT className for color). +- Use `SafeAreaView` from `react-native-safe-area-context` as the top-level wrapper of every screen. +- Wrap text inputs in `KeyboardAvoidingView` patterns when they can be covered by the keyboard. +- Use `expo-haptics` for tactile feedback on meaningful actions; `expo-linear-gradient` for gradients (NOT CSS gradients). +- Animations: use `react-native-reanimated` (already configured). CSS transitions/animations do not exist. + + + +The app uses expo-router (file-based routing): +- Every file in `app/` is automatically a route: `app/index.jsx` → `/`, `app/settings.jsx` → `/settings`, `app/items/[id].jsx` → `/items/:id`. +- `app/_layout.jsx` is the root layout (Stack). For tab navigation create `app/(tabs)/_layout.jsx` with `Tabs` from expo-router and move tab screens into `app/(tabs)/`. +- Navigate with the `` component or `router.push("/settings")` from `import { router } from "expo-router"`. +- Read route params with `useLocalSearchParams()`. +- NEVER use react-router-dom — it does not exist in this project. + + + +Styling uses NativeWind v4 — Tailwind classes via the `className` prop on React Native components: +- `className="flex-1 items-center justify-center bg-slate-50"` works on View/Text/etc. +- Flexbox is the ONLY layout system (no CSS grid, no floats). `flex-row` for horizontal, default is column. +- Text styling classes (`text-lg`, `font-bold`, `text-slate-900`) go on ``, not on the wrapping ``. +- No hover states on touch devices — use Pressable's `({ pressed })` style callback for press feedback. +- Tailwind purges classes it cannot find as literal substrings in source at build time. Never construct class names dynamically (e.g. `bg-${color}-500`). Literal class strings in source — including inside const maps/arrays — ARE scanned. +- Shadows: use `shadow-sm`/`shadow-md` sparingly (they translate to native shadows); prefer borders and background contrast for depth. + + + + +Only the following packages are installed: +- react / react-native +- expo (and expo-constants, expo-linking, expo-status-bar, expo-haptics, expo-linear-gradient) +- expo-router (file-based navigation) +- expo-image (use instead of the RN Image for remote images) +- nativewind + tailwindcss (styling via className) +- react-native-reanimated (animations) +- react-native-gesture-handler (gestures; root view already wired) +- react-native-safe-area-context (SafeAreaView) +- react-native-screens +- react-native-svg +- @shopify/flash-list (performant lists) +- lucide-react-native (icons) +- @tanstack/react-query (data fetching and query state) +- date-fns +- lodash +- zod +- @/api/base44Client (pre-initialized Base44 SDK) + +EXTREMELY IMPORTANT: DO NOT USE ANY OTHER LIBRARIES other than those listed above. This will BREAK THE APP. In particular: no react-dom, no react-router-dom, no shadcn/ui, no framer-motion, no recharts, no moment. + + + +The entity SDK is documented in the `base44-platform-entities` skill (mobile reference). + + + +The integrations SDK is documented in the `base44-platform-integrations-sdk` skill (mobile reference). + + + + +You always create beautiful apps, with a clean and modern look that feels native to mobile. +Design mobile-first: generous touch targets (min 44pt), comfortable spacing, bottom-of-thumb reachability for primary actions. + +IMPORTANT: For all designs I ask you to make, have them be beautiful, not cookie cutter. +Make screens that are fully featured and worthy of a real app store app. + + +Unless the user asks for a different style, follow iOS design conventions — apps should feel at home on an iPhone: +- Typography hierarchy like iOS: one large bold screen title (text-3xl/4xl font-bold tracking-tight), 17pt-equivalent body (text-[17px]), 13pt-equivalent captions (text-[13px]). +- CONTRAST IS NON-NEGOTIABLE: primary text near-black (e.g. text-zinc-900) on light backgrounds; secondary text no lighter than zinc-500. NEVER set body or interactive text in washed-out tints — muted colors are for captions and disabled states only. +- One vivid accent color used decisively (iOS-style tints like blue #007AFF, indigo #5856D6, green #34C759) for primary buttons, active states, and links — at full saturation, not pastel. +- Surfaces: grouped-card look — white cards (rounded-2xl) on a faint gray page background (e.g. bg-zinc-100), hairline separators (border-zinc-200), subtle shadows only. +- Primary action buttons: filled with the accent color, white bold label, rounded-xl/2xl, 50pt tall — they should be impossible to miss. +- Spacing: 16-20pt screen padding, breathing room between sections; avoid cramped layouts. +- SF-Symbols-like iconography: lucide-react-native icons at consistent sizes (20-24), colored with the accent or zinc-400 — never near-invisible tints. + + +Use loading indicators (`ActivityIndicator`) when fetching data or using integrations. +Respect safe areas on every screen (notches, home indicator). + + + +- Prioritize creating small, focused files and components. +- Create a new file for every new component, under `src/components/` (screens live in `app/`). +- Aim for components that are 50 lines of code or less. +- When a screen's code gets too big, break it down into components. + + + +You work on the app's sandbox files directly: read, write and edit them by path and run shell commands, through the Base44 MCP sandbox tools or the `base44 sandbox` CLI. +- Entity files (`base44/entities/*.jsonc`) are stored as objects: always write the complete schema, never a partial patch or placeholders. +- Writes are committed automatically a few seconds after the change and synced into the platform: a resource file (entity, agent, function, workflow) is live once written; there is no deploy step. +- Only checkpoints appear in the builder's version history. Create a checkpoint when you finish a unit of work and before you stop; the next Restore in the builder discards everything after the last checkpoint. +- Verify with the project's own commands (`npm run build`, lint) before finishing. + + + + +Only install when user explicitly requests by name or npmjs.com URL. Use existing / installed packages otherwise. +Install exact package requested - do not substitute with alternatives. + + + +1. Check the installed packages list to verify not already installed +2. Install from the sandbox shell: `npm install @^` (for Expo native modules prefer `npx expo install `); stable versions, caret (^) for compatible updates. Remove a package the user no longer wants with `npm uninstall `. Never uninstall @base44/sdk or @base44/vite-plugin — the platform requires them. + + diff --git a/skills/base44-platform-frontend/references/web-spa.md b/skills/base44-platform-frontend/references/web-spa.md new file mode 100644 index 0000000..018e858 --- /dev/null +++ b/skills/base44-platform-frontend/references/web-spa.md @@ -0,0 +1,118 @@ + +# Base44 frontend: Web app, Vite SPA + +You develop pages and, when needed, components or the app's layout. + + +- Before editing, account for dependent files, earlier changes, and the user's own edits. +- Export every page/component as default, named same as its file. +- shadcn/ui from @/components/ui; Tailwind CSS for styling. +- Icons: lucide-react only, only icons that exist — a nonexistent or unimported icon breaks the whole app. +- Every import must resolve to a real file or package. +- URL params: const urlParams = new URLSearchParams(window.location.search). +- Handle just-registered users with nothing saved on the user entity yet. +- Entities: always the full JSON schema — no comments/placeholders; it replaces the stored schema. +- Let errors bubble up — no try/catch unless asked, since uncaught errors come back to you to fix. Exception: user-facing form/auth flows catch and show inline errors (that behavior is the feature). + + + + +Only these frontend packages are installed: React; tailwind css; shadcn/ui (all components); lucide-react; moment; recharts; react-quill-new; react-hook-form; react-router-dom; date-fns; lodash; react-markdown; framer-motion; three.js (3d models/games); react-leaflet (maps); @hello-pangea/dnd (drag and drop); @tanstack/react-query (data fetching/query state); @/api/base44Client (pre initialized sdk); @/utils. +Never use any other library — only this list plus shadcn components in @/components/ui/; anything else breaks the app. + + + +The entity SDK is documented in the `base44-platform-entities` skill. + + + +The integrations SDK (InvokeLLM, SendEmail, file uploads) is documented in the `base44-platform-integrations-sdk` skill. + + + +Analytics tracking is documented in the `base44-platform-integrations-sdk` skill. + + + + +src/App.jsx is the application router — a page without a Route there is unreachable. On page add/remove, update src/App.jsx in the same change: import near "// Add page imports here" + Route inside Routes. + + + with route paths from src/App.jsx, not createPageUrl. + + + +No automatic layout wrapping — don't create a root-level src/Layout.jsx. Shared layouts: a component (e.g., src/components/Layout.jsx) with React Router's , used as a layout route: + +```jsx +import { Outlet } from 'react-router-dom'; +export default function Layout() { return
; } +// src/App.jsx +}> + } /> + +``` +
+ + +- Main/home page on "/" only — no second route under the component name (both "/" and "/Home"). +- Changing the main page: the "/" route in src/App.jsx is the source of truth — no platform setting; point "/" at the desired component and import it at the top (don't send the user to dashboard settings). + + +Keep these framework wrappers in src/App.jsx intact — auth, data fetching, routing, and toasts run through them: +- , , /, +- AuthenticatedApp and its auth logic (loading states, UserNotRegisteredError) +- ProtectedRoute from @/components/ProtectedRoute for per-route auth guarding + +
+ + +Write Tailwind classes as literal strings (const maps/arrays included) — the build purges anything not found as a literal substring in source; dynamic names (`bg-${color}-500`, `"bg-" + shade`) silently disappear. `safelist` in `tailwind.config.js` only for runtime-sourced values (entity records, API responses, user input), never for classes in source. + + + + +index.html — for page title, meta tags (SEO, Open Graph), external scripts (Analytics, AdSense), favicon, custom fonts. Always preserve: `` (runtime script injection), `
` (React mount), `` (entry). Once customized, served as-is — no automatic injections (SEO/social tags, canonical URLs, favicons). +
+
+ + +Build beautiful, clean, modern, fully featured, production-worthy, responsive (mobile + desktop) apps. + +Stock photos from Unsplash where appropriate — only valid URLs you know exist; link in image tags, don't download. + + +Render every content image (user uploads, generated images — any media.base44.com / static.wixstatic.com URL) with the `Image` component from `@/components/ui/image`, never a plain `` — it serves the image resized to its container, encoded for the browser, with responsive srcset. Size the box via `className`; `fittingType="fill"` (default, crops) or `"fit"` (contains); optional `focalPointX`/`focalPointY` (0–1) anchor the crop. Other hosts and SVGs pass through unchanged, so it is always safe. + + +Show loading indicators while integration calls are in flight. +Color token syntax must match its consumer: `hsl(var(--token))` requires HSL channels such as `0 0% 100%`; an RGB tuple such as `248 250 252` requires `rgb(var(--token))`. Never put unitless RGB channels into an HSL token. + + +- `src/index.css` owns token values; `tailwind.config.js` maps tokens to Tailwind classes. Theme changes: update values in `src/index.css`, use mapped classes in JSX. +- Colors: update both `:root` and `.dark`. +- Fonts: `@import url('...')` at top of `src/index.css`, before `@tailwind base;`; update font role tokens (`--font-heading`, `--font-body`, `--font-display`), adding them to `src/index.css`/`tailwind.config.js` if missing. +- JSX: mapped token classes (`bg-primary`, `font-heading`, `font-body`, `font-display`); no hardcoded values (`bg-[#ffffff]`, `bg-white`, `font-[Inter]`, inline `style={{ background: "#ffffff", fontFamily: "Inter" }}`). +- New custom tokens only when semantic tokens aren't enough and the value is reused: define under `:root` + `.dark` in `src/index.css`, map in `tailwind.config.js`, use the generated class. + + + + + +- Small focused files: components of 50 lines or less; every new component/page gets its own new file, never added to an existing one. +- Break oversized pages into components yourself — no need to ask. + + + +You work on the app's sandbox files directly: read, write and edit them by path and run shell commands, through the Base44 MCP sandbox tools or the `base44 sandbox` CLI. +- Entity files (`base44/entities/*.jsonc`) are stored as objects: always write the complete schema, never a partial patch or placeholders. +- Writes are committed automatically a few seconds after the change and synced into the platform: a resource file (entity, agent, function, workflow) is live once written; there is no deploy step. +- Only checkpoints appear in the builder's version history. Create a checkpoint when you finish a unit of work and before you stop; the next Restore in the builder discards everything after the last checkpoint. +- Verify with the project's own commands (`npm run build`, lint) before finishing. + + + +Install only when the user explicitly requests a package by name or npmjs.com URL — the exact package, no substitutes; otherwise use installed packages. Check the installed list first, then install from the sandbox shell (`npm install @^`): stable versions, caret (^) for compatible updates. Remove a package the user no longer wants with `npm uninstall`. Never uninstall @base44/sdk or @base44/vite-plugin — the platform requires them. + diff --git a/skills/base44-platform-frontend/references/web-tanstack.md b/skills/base44-platform-frontend/references/web-tanstack.md new file mode 100644 index 0000000..4f31cbd --- /dev/null +++ b/skills/base44-platform-frontend/references/web-tanstack.md @@ -0,0 +1,212 @@ + +# Base44 frontend: Web app, TanStack Start + +You develop pages and, when needed, components or the app's layout. + + +- Before editing, account for dependent files, earlier changes, and the user's own edits. +- Route files export `Route` (components are plain functions in the same file); every other component is a default export named same as its file. +- shadcn/ui from @/components/ui; Tailwind CSS for styling. +- Icons: lucide-react only, only icons that exist — a nonexistent or unimported icon breaks the whole app. +- Every import must resolve to a real file or package. +- URL params: `Route.useParams()` for path params, `validateSearch` + `Route.useSearch()` for the query string — never `window.location` in render. +- Handle just-registered users with nothing saved on the user entity yet. +- Entities: always the full JSON schema — no comments/placeholders; it replaces the stored schema. +- Let errors bubble up — no try/catch unless asked, since uncaught errors come back to you to fix. Exception: user-facing form/auth flows catch and show inline errors (that behavior is the feature). + + + + +Only these packages are installed: React 19; tailwind css; shadcn/ui (all components); lucide-react; moment; date-fns; lodash; recharts; react-hook-form + @hookform/resolvers + zod; react-day-picker; react-markdown; react-quill-new; react-hot-toast; sonner (toasts, via @/components/ui/sonner); next-themes; framer-motion; three.js (3d models/games); react-resizable-panels; react-leaflet (maps); @hello-pangea/dnd (drag and drop); canvas-confetti; html2canvas; jspdf; @stripe/stripe-js + @stripe/react-stripe-js; @tanstack/react-router; @tanstack/react-start; @tanstack/react-query (data fetching/query state); @base44/sdk; @/api/base44Client (pre-initialized browser sdk); @/lib/utils (`cn`). +Never use any other library — only this list plus shadcn components in @/components/ui/; anything else breaks the app. + + + +The entity SDK is documented in the `base44-platform-entities` skill. + + + +The integrations SDK (InvokeLLM, SendEmail, file uploads) is documented in the `base44-platform-integrations-sdk` skill. + + + +Analytics tracking is documented in the `base44-platform-integrations-sdk` skill. + + + + +Routing is file-based: every file under `src/routes/` is a route. `src/routeTree.gen.ts` is generated on every dev/build run: never write, edit or delete it, and write only `.js`/`.jsx` files — this app is JavaScript/JSX. +- `__root.jsx` — the document and providers (see ). +- `index.jsx` → `/` · `about.jsx` → `/about` · `posts.index.jsx` → `/posts` · `posts.$postId.jsx` → `/posts/:postId` · `posts.$postId_.edit.jsx` → `/posts/:postId/edit` · `$.jsx` catch-all. +- Dots and folders nest: `posts.jsx` is the parent of every `posts.*` route, as `posts/$postId.jsx` is of `posts/$postId/edit.jsx`, and a parent shows its child only through `` — without it the child URL silently renders the parent page. So list/detail/edit pages are siblings: `posts.index.jsx` + `posts.$postId.jsx` + `posts.$postId_.edit.jsx`, no `posts.jsx` (add one only as a shared layout with ``). A trailing `_` (`posts_.$postId.jsx`, `$postId_.edit`) opts out of the parent. +- `_authed.jsx` + `_authed/dashboard.jsx` — pathless layout: wraps its children (`/dashboard`) without adding a URL segment; `_authed.jsx` holds the login gate (`beforeLoad: requireSignedIn` from `@/components/auth/AuthGate`, see ), and its children read `Route.useRouteContext().user`. `(marketing)/pricing.jsx` — route group: organizes files, adds no segment. A folder (`posts/index.jsx`) equals the dotted name. +- `api/.js` — server routes (see ). +- Every route file: `export const Route = createFileRoute("/path")({ component, loader, head, … })` from `@tanstack/react-router`; the path string is the file's URL, and the plugin rewrites it on rename. + + + +- Reads: a `loader` (runs on the server for a document request, in the browser on client navigation) calling a server function; the component reads it with `Route.useLoaderData()`. `loaderDeps: ({ search }) => ({ … })` when the loader depends on search params. +- Writes: `useServerFn(fn)` + TanStack Query `useMutation`, then `useRouter().invalidate()` (or `queryClient.invalidateQueries`) so lists refresh. +- Page metadata: `head: () => ({ meta: [{ title }, { name: "description", content }], links: [...] })` per route — never a `` in JSX. +- `ssr: false` only on a route whose render or loader needs the signed-in user (`useAuth()`, `base44.auth.me()`, the user's own records), so personal data never lands in cacheable HTML, and on the auth pages and `oauth/consent`, wired with `ssr: false` as <authentication> shows. Every other page stays server-rendered — public pages anyone can open and pages that only keep browser state (localStorage, theme, drafts); `ssr: false` is never the fix for `window` or hydration. A library that touches `window`/`document` on import (maps, rich-text editors) goes in its own component, rendered as `lazy(() => import("@/components/Map"))` inside `<ClientOnly fallback={…}>` from `@tanstack/react-router`. Public pages may opt into edge caching with the route's `headers: () => ({ "cache-control": "public, s-maxage=…" })`. +- Auth is resolved client-side after hydration: `useAuth()` from `@/lib/AuthContext` (`user`, `isAuthenticated`, `isLoadingAuth`, `refresh`, `logout`, `navigateToLogin`). Gate user-specific UI on `isLoadingAuth`; the server renders every page anonymously. +- Search params: make each `validateSearch` param optional and apply its default where it is read, since a default returned from `validateSearch` redirects `/` to `/?param=…`. +- Server and first client render must match, or React throws a hydration error (a bug to fix, not a warning). `window`, `document` and browser storage only inside effects and handlers — never at module scope, in render or in a `useState` initializer, even behind `typeof window`: render the server's default, then load the stored value in `useEffect`. Format dates and numbers with an explicit locale and time zone (`toLocaleString("en-US", { timeZone: "UTC" })`); render `Date.now()`/random values only after mount. +</data_and_rendering> + +<navigation> +`<Link to="/posts/$postId" params={{ postId }}>` and `useNavigate()` from `@tanstack/react-router`; paths are the file names above — never `createPageUrl`, `react-router-dom` or `<a href>` for in-app links. +Every screen, tab and section a user can navigate to is its own route file with its own URL and `head()` — never one route switching screens with `useState` or a `?tab=` param. +The main/home page (`/`) is exactly one file: `src/routes/index.jsx`, or `_authed/index.jsx` / `_site/index.jsx` once it moves under a layout (move it, never copy it, so two files never claim `/`); changing the main page means editing that file — no platform setting. The starter's home page is a placeholder: replace the whole file rather than patching it. +The starter has no app chrome. A shared header, nav or sidebar is a component (e.g. `src/components/Layout.jsx`) rendered around `<Outlet />` in a layout route's `component`: `__root.jsx` while the app has no auth routes; `_authed.jsx` once it has them, with its gated pages under `_authed/`; and if it also keeps public pages, a pathless `_site.jsx` rendering the same layout component, with those pages (the home page included) under `_site/`. The auth pages and `oauth/consent` stay outside every such layout and render full-screen. +</navigation> + +<tailwind_class_names> +Write Tailwind classes as literal strings (const maps/arrays included) — the build purges anything not found as a literal substring in source; dynamic names (`bg-${color}-500`, `"bg-" + shade`) silently disappear. `safelist` in `tailwind.config.js` only for runtime-sourced values (entity records, API responses, user input), never for classes in source. +</tailwind_class_names> + +<prebuilt_components> +<root_document> +`src/routes/__root.jsx` renders the document and providers. Site-wide title/meta/favicon/font links go in its `head()`; per-page ones in each route's `head()`. External scripts (Analytics, AdSense) go in `<body>` next to `<Base44Scripts />`, never in `head()` — the preview proxy prepends its own scripts to `<head>`, which breaks hydration of any script rendered there. Always preserve `data-react-root` on `<body>` and the order `<HeadContent />`, `<AuthProvider>`, `<Toaster />`, `<Base44Scripts />` immediately before `<Scripts />` — the platform's runtime hooks depend on it. +</root_document> +</prebuilt_components> + +<styling_instructions> +Build beautiful, clean, modern, fully featured, production-worthy, responsive (mobile + desktop) apps. + +Stock photos from Unsplash where appropriate — only valid URLs you know exist; link in image tags, don't download. + +<content_image_component> +Render every content image (user uploads, generated images — any media.base44.com / static.wixstatic.com URL) with the `Image` component from `@/components/ui/image`, never a plain `<img>` — it serves the image resized to its container, encoded for the browser, with responsive srcset. Size the box via `className`; `fittingType="fill"` (default, crops) or `"fit"` (contains); optional `focalPointX`/`focalPointY` (0–1) anchor the crop. Other hosts and SVGs pass through unchanged, so it is always safe. +</content_image_component> + +Show loading indicators while integration calls are in flight. +Color token syntax must match its consumer: `hsl(var(--token))` requires HSL channels such as `0 0% 100%`; an RGB tuple such as `248 250 252` requires `rgb(var(--token))`. Never put unitless RGB channels into an HSL token. + +<tokens_based_design_system> +- `src/index.css` owns token values; `tailwind.config.js` maps tokens to Tailwind classes. Theme changes: update values in `src/index.css`, use mapped classes in JSX. +- Colors: update both `:root` and `.dark`. +- Fonts: `@import url('...')` at top of `src/index.css`, before `@tailwind base;`; update font role tokens (`--font-heading`, `--font-body`, `--font-display`), adding them to `src/index.css`/`tailwind.config.js` if missing. +- JSX: mapped token classes (`bg-primary`, `font-heading`, `font-body`, `font-display`); no hardcoded values (`bg-[#ffffff]`, `bg-white`, `font-[Inter]`, inline `style={{ background: "#ffffff", fontFamily: "Inter" }}`). +- New custom tokens only when semantic tokens aren't enough and the value is reused: define under `:root` + `.dark` in `src/index.css`, map in `tailwind.config.js`, use the generated class. +</tokens_based_design_system> + +</styling_instructions> + +<componentization> +- Small focused files: components of 50 lines or less; every new component/page gets its own new file, never added to an existing one. +- Break oversized pages into components yourself — no need to ask. +</componentization> + +<working_with_files> +You work on the app's sandbox files directly: read, write and edit them by path and run shell commands, through the Base44 MCP sandbox tools or the `base44 sandbox` CLI. +- Entity files (`base44/entities/*.jsonc`) are stored as objects: always write the complete schema, never a partial patch or placeholders. +- Writes are committed automatically a few seconds after the change and synced into the platform: a resource file (entity, agent, function, workflow) is live once written; there is no deploy step. +- Only checkpoints appear in the builder's version history. Create a checkpoint when you finish a unit of work and before you stop; the next Restore in the builder discards everything after the last checkpoint. +- Verify with the project's own commands (`npm run build`, lint) before finishing. +</working_with_files> + +<npm_package_installation> +Install only when the user explicitly requests a package by name or npmjs.com URL — the exact package, no substitutes; otherwise use installed packages. Check the installed list first, then install from the sandbox shell (`npm install <package>@^<version>`): stable versions, caret (^) for compatible updates. Remove a package the user no longer wants with `npm uninstall`. Never uninstall @base44/sdk or @base44/vite-plugin — the platform requires them. +</npm_package_installation> + +<example> +Two short worked examples; each file is shown with its path. `…` elides boilerplate. + +<user_ask>Create a simple TODO app where I can track my tasks</user_ask> +<assistant> +``` +// base44/entities/Task.jsonc +{"name":"Task","type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string","enum":["todo","in_progress","done"],"default":"todo"},"priority":{"type":"string","enum":["low","medium","high"],"default":"medium"},"due_date":{"type":"string","format":"date"}},"required":["title"]} +``` +<note>Schema first; then all independent files in one parallel batch:</note> +``` +// src/lib/server-fns.js +import { createServerFn } from "@tanstack/react-start"; +import { z } from "zod"; + +const TaskInput = z.object({ title: z.string().min(1), description: z.string().optional(), status: z.enum(["todo", "in_progress", "done"]).optional(), priority: z.enum(["low", "medium", "high"]).optional(), due_date: z.string().optional() }); +export const listTasks = createServerFn({ method: "GET" }).handler(({ context }) => + context.getBase44().entities.Task.list("-created_date", 50)); +export const createTask = createServerFn({ method: "POST" }) + .validator(TaskInput) + .handler(({ data, context }) => context.getBase44().entities.Task.create(data)); +export const updateTask = createServerFn({ method: "POST" }) + .validator(TaskInput.partial().extend({ id: z.string() })) + .handler(({ data: { id, ...patch }, context }) => context.getBase44().entities.Task.update(id, patch)); +… // deleteTask: same shape, called as deleteTask({ data: { id } }) +``` +``` +// src/components/tasks/TaskForm.jsx +import React from "react"; +import { Button } from "@/components/ui/button"; +… // controlled form for title/status/priority/due_date, onSubmit(currentTask) +``` +``` +// src/components/tasks/TaskItem.jsx +… // one task row: status toggle, priority badge, edit/delete actions +``` +``` +// src/routes/index.jsx +import { createFileRoute, useRouter } from "@tanstack/react-router"; +import { useServerFn } from "@tanstack/react-start"; +import { listTasks, createTask, updateTask } from "@/lib/server-fns"; +import TaskForm from "@/components/tasks/TaskForm"; +import TaskItem from "@/components/tasks/TaskItem"; + +export const Route = createFileRoute("/")({ + ssr: false, // the signed-in user's own tasks — a public page (home, listing, detail anyone can open) omits this + loader: () => listTasks(), + head: () => ({ meta: [{ title: "Tasks" }] }), + component: TasksPage, +}); + +function TasksPage() { + const tasks = Route.useLoaderData(); + const router = useRouter(); + const create = useServerFn(createTask); + const update = useServerFn(updateTask); + … // renders form + filtered list, full CRUD wired: await create({ data: form }), await update({ data: { id: task.id, ...patch } }), then router.invalidate() +} +``` + +Your todo app is ready — add tasks, set priority and due dates, and move them between To Do, In Progress, and Done. +</assistant> + +<user_ask>show each item on its own page with its full details</user_ask> +<assistant> +<note>One focused route — exactly the ask, no extra sections/pages/animations. Add the server function it needs, load by id in the loader, and render every stored field (a view that drops the item's main text/body is incomplete).</note> +``` +// src/lib/server-fns.js +… // existing server functions unchanged +export const getTask = createServerFn({ method: "GET" }) + .validator(z.object({ id: z.string() })) + .handler(({ data, context }) => context.getBase44().entities.Task.get(data.id) + .catch((error) => { if (error.status === 404) return null; throw error; })); +``` +``` +// src/routes/tasks.$taskId.jsx +import { createFileRoute, notFound } from "@tanstack/react-router"; +import { getTask } from "@/lib/server-fns"; + +export const Route = createFileRoute("/tasks/$taskId")({ + ssr: false, // the user's own task + loader: async ({ params }) => { + const task = await getTask({ data: { id: params.taskId } }); + if (!task) throw notFound(); + return task; + }, + head: ({ loaderData }) => ({ meta: [{ title: loaderData?.title ?? "Task" }] }), + component: TaskDetail, +}); + +function TaskDetail() { + const task = Route.useLoaderData(); + … // render the title and the full body/content — wrap content in the right component and pass the value in, never leave it empty +} +``` +Each item now opens on its own page with its full details. +</assistant> +</example> diff --git a/skills/base44-platform-games/SKILL.md b/skills/base44-platform-games/SKILL.md new file mode 100644 index 0000000..122bd54 --- /dev/null +++ b/skills/base44-platform-games/SKILL.md @@ -0,0 +1,66 @@ +--- +name: base44-platform-games +description: "Base44 games: Phaser 4 (2D) and Three.js via React Three Fiber (3D) reference material for game apps built on Base44. Use when implementing scenes, physics, input, cameras, assets, HUD or game state in a Base44 game." +--- +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games + +A Base44 game is a React app with the game layer under `src/game/`; React owns the screen-space overlay and the Base44 SDK, the engine owns gameplay. Each topic reference covers one engine area. + +## Pick the reference for this app + +Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (over the Base44 MCP `execute_api` +tool, or with your Base44 CLI token); it returns the framework, app type, runtime, +entitlements and the exact reference files to read. Without it, detect from the project files: +`src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a +native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. + +| Profile | Read | +|---|---| +| Game, 2D (Phaser 4) | [2D game (Phaser 4): architecture, assets, analytics and the Phaser reference](references/game-2d.md) | +| Game, 3D (Three.js) | [3D game (Three.js): architecture, assets, analytics and the Three.js reference](references/game-3d.md) | + +## Topic references + +- [Phaser 4: actions and utilities](references/phaser/actions_and_utilities.md) +- [Phaser 4: animations](references/phaser/animations.md) +- [Phaser 4: audio and sound](references/phaser/audio_and_sound.md) +- [Phaser 4: cameras](references/phaser/cameras.md) +- [Phaser 4: curves and paths](references/phaser/curves_and_paths.md) +- [Phaser 4: data manager](references/phaser/data_manager.md) +- [Phaser 4: events system](references/phaser/events_system.md) +- [Phaser 4: filters and postfx](references/phaser/filters_and_postfx.md) +- [Phaser 4: game object components](references/phaser/game_object_components.md) +- [Phaser 4: game setup and config](references/phaser/game_setup_and_config.md) +- [Phaser 4: geometry and math](references/phaser/geometry_and_math.md) +- [Phaser 4: graphics and shapes](references/phaser/graphics_and_shapes.md) +- [Phaser 4: groups and containers](references/phaser/groups_and_containers.md) +- [Phaser 4: input keyboard mouse touch](references/phaser/input_keyboard_mouse_touch.md) +- [Phaser 4: loading assets](references/phaser/loading_assets.md) +- [Phaser 4: mesh and 3d elements](references/phaser/mesh_and_3d_elements.md) +- [Phaser 4: particles](references/phaser/particles.md) +- [Phaser 4: physics arcade](references/phaser/physics_arcade.md) +- [Phaser 4: physics matter](references/phaser/physics_matter.md) +- [Phaser 4: render textures](references/phaser/render_textures.md) +- [Phaser 4: scale and responsive](references/phaser/scale_and_responsive.md) +- [Phaser 4: scenes](references/phaser/scenes.md) +- [Phaser 4: sprites and images](references/phaser/sprites_and_images.md) +- [Phaser 4: text and bitmaptext](references/phaser/text_and_bitmaptext.md) +- [Phaser 4: tilemaps](references/phaser/tilemaps.md) +- [Phaser 4: time and timers](references/phaser/time_and_timers.md) +- [Phaser 4: tweens](references/phaser/tweens.md) +- [Three.js: assets models textures](references/threejs/assets_models_textures.md) +- [Three.js: cameras](references/threejs/cameras.md) +- [Three.js: character controller](references/threejs/character_controller.md) +- [Three.js: enemies and movement](references/threejs/enemies_and_movement.md) +- [Three.js: game state and flow](references/threejs/game_state_and_flow.md) +- [Three.js: input and controls](references/threejs/input_and_controls.md) +- [Three.js: performance](references/threejs/performance.md) +- [Three.js: physics dynamics](references/threejs/physics_dynamics.md) +- [Three.js: projectiles and pooling](references/threejs/projectiles_and_pooling.md) +- [Three.js: scene and rendering](references/threejs/scene_and_rendering.md) +- [Three.js: text hud ui](references/threejs/text_hud_ui.md) +- [Three.js: triggers and collisions](references/threejs/triggers_and_collisions.md) +- [Three.js: vfx and juice](references/threejs/vfx_and_juice.md) diff --git a/skills/base44-platform-games/references/game-2d.md b/skills/base44-platform-games/references/game-2d.md new file mode 100644 index 0000000..92504ef --- /dev/null +++ b/skills/base44-platform-games/references/game-2d.md @@ -0,0 +1,329 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: 2D game (Phaser 4): architecture, assets, analytics and the Phaser reference + +<game_architecture> +The game project is a standard React/Base44 app with a game layer under `src/game/`. + +<file_structure> +The game template ships this skeleton ALREADY WIRED: `src/App.jsx` routes `/` to GamePage, `GamePage.jsx` renders `<GameContainer />` (the engine-agnostic mount that owns the `#game-container` div), and `src/game/main.js` re-exports the active engine flavor. Build the game by EDITING these files in place — never recreate them or build a parallel structure elsewhere. The skeleton files are small and self-documenting: read them before changing them instead of relying on a from-memory picture of their contents. + +- `src/game/2d/` - ALL gameplay code for this game: scenes, game objects, systems, spawning, scoring, enemy AI. The starter scene is `scenes/GameScene.js` — gameplay code starts THERE; add further scene files under `scenes/` and reusable game-object files (e.g. `objects/`) alongside it. +- `src/game/main.js` - one-line engine switch re-exporting the active flavor's `Game` component. +- `src/game/config.js` - create for tunable game constants (see configuration). +- `src/game/EventBus.js` - engine-agnostic event emitter for React <-> game communication; rarely needs editing. +- `src/pages/GamePage.jsx` - React page that mounts the game; edit to build the HUD/menu overlay. +- `src/components/game/GameContainer.jsx` - mount wrapper; rarely needs editing. +- `src/App.jsx` - React router and Base44 shell; already mounts GamePage at `/` — edit only to add MORE routes. +- `src/main.jsx` - React bootstrap. Do not edit this for game routing. + +An older game may have a flatter layout — no `2d/`/`3d/` flavor split, a `PhaserGame`/`ThreeGame` mount instead of `GameContainer`. Follow the existing wiring in that case; the placement and boundary rules below still apply. + +Placement rule: ALL gameplay code lives under `src/game/`. `src/components/game/` holds ONLY the mount wrapper. React overlay UI components go in `src/components/` (or inline in GamePage.jsx) and contain no gameplay logic. +</file_structure> + +<boundary_rules> +- Put Base44 SDK calls, auth-aware data loading, navigation, and ALL screen-space UI — start/pause/settings menus, game-over and victory screens, score/lives/level readouts, buttons, dialogs, leaderboards, forms — in React files as a DOM overlay over the canvas (see screen_space_ui below). Do not build these inside the game world. +- Put gameplay state, physics, collisions, animation, game input, cameras, render-loop behavior, and world-anchored text (floating damage numbers, speech bubbles above characters, pickup labels) in `src/game`. React overlay state is a display copy fed by events — never the authoritative home for gameplay data. +- Use `src/game/EventBus.js` for ALL React <-> game communication, in both directions: the game emits gameplay events (score changed, game over) that React UI renders; React emits commands (restart, pause, resume) that the game acts on. +- Persist durable game data (high scores, saves, progress, leaderboards) through Base44 entities from React — never call the SDK inside the game loop or a scene; bridge gameplay events to React via `EventBus`. +- Preserve the `current-scene-ready` event pattern: the active scene emits it once it is mounted so `GameContainer` can track its handle. This event does NOT mean the first playable frame rendered and must not drive `window.game.ready`. +- When adding scenes, register them in the `scene` array of the Phaser config (`src/game/2d/main.js`). +</boundary_rules> + +<screen_space_ui> +Decision rule: if an element would look and behave correctly as a DOM element positioned over the canvas with CSS, build it in React (Tailwind + shadcn/ui). If it must track a world position, move with the camera, or update every frame in lockstep with rendering, build it in the game world. +- React overlay: menus, pause/game-over/victory screens, score-lives-level HUD readouts, buttons, settings, instructions, dialogs, leaderboards, forms. +- Game world: damage numbers, world-anchored labels and speech bubbles, and per-frame readouts tightly coupled to the render loop. + +Overlay structure: overlay elements are SIBLINGS of `<GameContainer />` inside the game frame wrapper (`.game-frame` is already `position: relative`). The overlay layer is `absolute inset-0 pointer-events-none` so gameplay clicks reach the canvas; interactive children re-enable `pointer-events-auto`: +```jsx +<div className="game-frame"> + <GameContainer /> + <div className="absolute inset-0 pointer-events-none"> + <div className="absolute top-3 left-3 text-white text-base sm:text-xl font-bold">Score: {score}</div> + {phase === 'game-over' && ( + <div className="absolute inset-0 grid place-items-center bg-black/60 pointer-events-auto"> + <Button onClick={() => EventBus.emit('restart-game')}>Play Again</Button> + </div> + )} + </div> +</div> +``` + +Two-way EventBus contract: +- Game -> React (state): emit on DISCRETE gameplay events only — `EventBus.emit('score-changed', score)` when the score actually changes, `EventBus.emit('game-over', { score })` once. NEVER emit from the per-frame update loop to drive React state; a value that changes every frame stays rendered in the game world, or its emit is throttled (e.g. every 100-250ms). +- Ticking overlay state (run timers, cooldown readouts — anything on an interval or throttled emit) lives in a LEAF component that owns its own state, never in the page component: overlay components are React ancestors of the game mount, so state there re-renders the Canvas/game subtree every tick and the whole scene reconciles at that rate. +- React -> Phaser (commands): the scene registers listeners in `create()` and MUST remove them on shutdown — EventBus outlives scenes, so leftover listeners stack up on every restart: +```js +// in GameScene.create() +const onRestart = () => this.scene.restart(); +const onPause = () => this.scene.pause(); +const onResume = () => this.scene.resume(); +EventBus.on('restart-game', onRestart); +EventBus.on('pause-game', onPause); +EventBus.on('resume-game', onResume); +this.events.once('shutdown', () => { + EventBus.off('restart-game', onRestart); + EventBus.off('pause-game', onPause); + EventBus.off('resume-game', onResume); +}); +``` +- React side: subscribe in `useEffect` with stable named handlers and matching `EventBus.off` cleanup. + +Overlay fonts: the template maps `font-display`, `font-heading`, `font-body`, and `font-mono` to `--font-*` CSS variables — to use a custom font, add `@import url('...')` at the very top of `src/index.css` (before `@tailwind base;`) and change the matching `--font-*` variable in `:root`. Never `@apply` a class that is not a built-in Tailwind utility or one mapped in `tailwind.config.js` — the build fails with "The `X` class does not exist". +Overlay color token syntax must match its consumer: `hsl(var(--token))` requires HSL channels such as `0 0% 100%`; an RGB tuple such as `248 250 252` requires `rgb(var(--token))`. Never put unitless RGB channels into an HSL token. +</screen_space_ui> + +<import_path_rules> +Import app and game modules with the `@/` alias rooted at `src/`. This is the style the skeleton files already use — mirror it in every file you create or edit. + +- Examples: `import { EventBus } from '@/game/EventBus';`, `import { GameScene } from '@/game/2d/scenes/GameScene';`, `import { GameContainer } from '@/components/game/GameContainer';`, `import { SHOP_ITEMS } from '@/game/config';`, `import { base44 } from '@/api/base44Client';`. +- A relative import (`./sibling`) is fine for a file in the same directory, but prefer `@/` for anything that crosses directories. +- Never import app modules with bare paths like `game/EventBus` or `components/game/GameContainer` — the build treats those as npm packages and fails to resolve them. +</import_path_rules> + +<configuration> +For non-trivial games, create `src/game/config.js` and keep tunable game constants there: +- dimensions, speeds, gravity, score values, colors, asset keys, art style, and perspective. +- Import constants from `src/game/config.js` into scenes and the engine setup. +- When the user asks to tune behavior ("faster", "more gravity", "larger world"), prefer editing config constants instead of scattering magic numbers. +</configuration> + +<testability> +- Every player- or AI-initiated gameplay capability must be callable independently of rendering and its physical input binding. Human-input and AI handlers are thin wrappers over the same gameplay functions. Represent sustained or analog controls with explicit control-state setters (press/release or normalized values); never add verification-only movement that bypasses the real update, timing, or physics path. Do not write the tests themselves +- Route all gameplay randomness through one seedable RNG (Phaser's game-config `seed` / `Phaser.Math.RND`). Vary the seed per run; a pinned seed is for reproducible verification. +- Expose and MAINTAIN the game API on `window.game`: `ready`, side-effect-free state getters (phase, score, key entities), and the capability/control functions required to exercise the primary playable loop. Initialize/reset `ready` to false before boot and restart; set it true only after required assets/async systems have finished and at least one playable frame rendered. Set readiness after preload/create completes and the following game render, not from `current-scene-ready` inside `create()`. Keep the API current as the game grows — a stale API is worse than none. +- HUD and menus stay in the React DOM overlay (see screen_space_ui) — that is the automated-verifiable surface. +</testability> +</game_architecture> + +<game_assets> +By default, build game visuals programmatically in code with Phaser's built-in primitives; for richer visuals than code shapes can produce (detailed characters, creatures, props, tiles, full backgrounds), generate images with the runtime `GenerateImage` integration or use assets the user provides, describing the project's `ART_STYLE` in every prompt so assets share one look, and store each image URL in `src/game/config.js` to load in `preload()` — reference the saved URL on every run rather than regenerating on each build. + +<programmatic_visuals> +Create visuals from code: +- Shapes: `this.add.rectangle(...)`, `this.add.circle(...)`, `this.add.triangle(...)`, `this.add.star(...)`. +- Graphics: `const g = this.add.graphics()` then `g.fillStyle(0x...)`, `g.fillRect(...)`, `g.fillCircle(...)`, `g.lineStyle(...)`, `g.strokePath(...)`. +- Reusable textures: draw with a Graphics object, bake it with `g.generateTexture('key', w, h)`, then use `'key'` in sprites, physics bodies, groups, particles, and animations. +- Text: `this.add.text(...)` (web/system fonts — no font files). +- Variety from a few textures: vary tint (`sprite.setTint(0x...)`), alpha, rotation, and scale. +</programmatic_visuals> + +<style_consistency> +Pick one cohesive look before drawing: a vibrant color palette and a shape language +(e.g. flat-geometric, neon, blocky), and keep it consistent across scenes. Store the +choices in `src/game/config.js` (e.g. `PALETTE`, `ART_STYLE`, `GAME_PERSPECTIVE`) when +that file exists or when you create it, and read from it everywhere. + +Perspective shapes how you draw and move things: +- `top-down` - camera looks straight down; flat/tiled ground, no horizon. +- `side-view` - side-scroller / platformer; a horizon and layered background are fine. +- `isometric` - 3/4 diamond-aligned view. +</style_consistency> + +<image_orientation> +A generated image is static — at runtime you can't tell which way it was drawn. So decide an object's facing +when you generate it, bake it into the image prompt, and write the movement code to match: say +"a fighter jet seen from above, nose pointing up", not just "a fighter jet". This matters for anything with a +heading (characters, vehicles, projectiles, creatures); backgrounds, tiling textures, and symmetric pickups +(coins, gems) don't need it. + +Choose the facing from how the object MOVES, not from what it is: +- Travels in a fixed direction without turning → draw it facing that direction. A vertical shooter's ship and + its bullets go up, so draw them pointing up; a runner's hero moves right, so draw it facing right. +- Turns to aim or steer (your code rotates it toward its target or velocity) → draw it facing right; the + rotation then needs no offset. E.g. a homing missile, a twin-stick player, a turret, a top-down car. +- Faces several ways (a walking character) → draw one facing and mirror it for the opposite side; generate + the other facings as separate images only if the game needs them. + +Enemies usually face the opposite way to the player, so they confront it: if the hero faces right, patrolling +enemies face left. (An enemy that chases by rotating toward the player is the "turns to aim" case above.) + +Examples by perspective: +- top-down: terrain and props seen from straight above; a shooter ship/asteroid reads from overhead (nose up); + top-down-RPG characters are drawn from a high angle so the face still shows, not a hat seen from above. +- side-view: side profiles on the ground — hero faces right, enemies face left, trees and buildings upright. +- isometric: drawn at the 3/4 iso angle, aligned to the diamond grid. +- UI/HUD icons: flat and front-on, no game perspective. +</image_orientation> + +<backgrounds> +Whether a background should REPEAT depends on whether the game SCROLLS — not on the +perspective. When generating or sourcing one, pick the matching +kind and wire the result accordingly: + +- Scrolling games (camera moves through a world larger than the screen — a platformer + level, a scrolling overworld): a large image with seamless edges, tiled with + `this.add.tileSprite(...)` and scrolled via `tilePosition*` with the camera. Top-down + tiles on both axes; side-view tiles horizontally only (a horizon must not repeat + vertically — stretch it to fill the height with `tileScaleY`). + +- Static, single-screen games (a board, an arena, a menu, a one-screen puzzle): do NOT + tile. Use a large single image that COVERS the viewport — one `this.add.image(...)` + scaled by `Math.max(GAME_WIDTH / w, GAME_HEIGHT / h)` (preserve aspect, crop overflow), + or `setDisplaySize(GAME_WIDTH, GAME_HEIGHT)` to stretch. + +Simple backgrounds can still be drawn programmatically — a solid fill, a Graphics +gradient, or shapes — matching the chosen perspective. +</backgrounds> + +Do not mix inconsistent visual styles in the same scene unless the user explicitly wants that contrast. +</game_assets> + +To opt-out, set `{"analytics":{"enabled":false}}` when initializing the base44 SDK (supported from 0.8.46). + +<phaser_reference> +Deep Phaser reference sections (physics, particles, tilemaps, etc.) are delivered inside user messages as <phaser_instructions> blocks when relevant. Sections from earlier messages in the conversation remain valid — rely on them even when the latest message carries none. + +<scene_basics> +Every Phaser scene must import Phaser or the Phaser classes it extends/uses. + +Valid patterns: +```js +import Phaser from 'phaser'; +export class GameScene extends Phaser.Scene {} +``` + +```js +import { Scene } from 'phaser'; +export class GameScene extends Scene {} +``` + +Scene lifecycle: +- `preload()` loads assets. +- `create()` creates objects, physics, input handlers, cameras, and emits readiness events. +- `update(time, delta)` runs every frame. +</scene_basics> + +<arcade_physics_default> +Default to Arcade physics for top-down games, platformers, runners, shooters, and most action games. + +Common Arcade patterns: +```js +this.physics.add.existing(sprite); +sprite.body.setVelocityX(200); +sprite.body.setGravityY(300); +sprite.body.setCollideWorldBounds(true); + +const platforms = this.physics.add.staticGroup(); +this.physics.add.collider(player, platforms); +this.physics.add.overlap(player, coins, collectCoin, null, this); +``` +</arcade_physics_default> + +<input> +Support keyboard and pointer/touch where reasonable. + +```js +this.cursors = this.input.keyboard.createCursorKeys(); +this.input.on('pointerdown', (pointer) => {}); +``` +</input> + +<cameras> +```js +this.cameras.main.startFollow(player); +this.cameras.main.setBounds(0, 0, worldWidth, worldHeight); +this.cameras.main.setZoom(1.5); +``` +</cameras> + +<animation> +```js +this.anims.create({ + key: 'walk', + frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }), + frameRate: 10, + repeat: -1, +}); +sprite.play('walk'); +``` +</animation> + +<tweens_timers_and_scenes> +```js +this.tweens.add({ targets: sprite, y: sprite.y - 50, duration: 500, yoyo: true }); +this.time.addEvent({ delay: 1000, callback: spawnEnemy, loop: true }); +this.scene.start('NextScene'); +this.scene.launch('EffectsScene'); // parallel world-layer scene — screen-space UI lives in the React overlay, not a Phaser scene +this.registry.set('score', 0); +this.registry.get('score'); +``` +</tweens_timers_and_scenes> + +<fallback_textures> +If an asset has not loaded yet, the game should still run. Create fallback textures before using keys: +```js +createFallbackTexture(key, width, height, color) { + if (this.textures.exists(key)) return; + const graphics = this.add.graphics(); + graphics.fillStyle(color, 1); + graphics.fillRect(0, 0, width, height); + graphics.generateTexture(key, width, height); + graphics.destroy(); +} +``` +</fallback_textures> + +<sprite_sheets> +Load sprite sheets with frame dimensions. Never load a sprite sheet as a single image. + +```js +this.load.spritesheet('player', '/assets/game/player.png', { + frameWidth: 256, + frameHeight: 256, +}); + +this.player = this.physics.add.sprite(400, 300, 'player', 0); +``` + +The sprite displays one frame at a time. Phaser slices the grid into frames. + +Common mistakes to avoid: +- Do not use `this.load.image()` for sprite sheets. +- Do not omit `{ frameWidth, frameHeight }`. +- Do not forget an initial frame when creating the sprite. +- Stop walk animations when idle, otherwise characters keep walking in place. +- Use `setFlipX` when reusing a side-facing row for left/right movement. +</sprite_sheets> + +<audio> +Browsers block autoplay. Start or resume audio after a user click/tap. +</audio> +</phaser_reference> + +<phaser_v4_guide> +This project uses Phaser 4 (the template ships `phaser@^4.1.0`). Several common Phaser 3 APIs were removed or changed in v4 — always use the v4 forms below, not the older v3 equivalents. These rules apply on EVERY turn; deeper per-topic detail arrives in the <phaser_instructions> sections of the user message when a topic is relevant. + +<v3_apis_removed_use_v4> +These Phaser 3 APIs are GONE in v4 — calling them throws or silently no-ops. Use the v4 form: + +- Mesh / Plane game objects are removed. There is NO `this.add.mesh()` or `this.add.plane()`, and `Phaser.Geom.Mesh` perspective helpers (`GenerateGridVerts`, `panZ`, `modelRotation`, `hideCCW`) are gone. For a 3D LOOK in a 2D Phaser game use pseudo-3D (depth scaling, parallax, isometric sorting) — see mesh_and_3d_elements. +- `setTintFill(color)` is removed → `sprite.setTint(color).setTintMode(Phaser.TintModes.FILL)`. Tint mode is now separate from tint color (modes: `MULTIPLY` (default), `FILL`, `ADD`, `SCREEN`, `OVERLAY`, `HARD_LIGHT`). +- `preFX` / `postFX` are removed → the Filters system. Call `sprite.enableFilters()` first (cameras already have filters), then add via `sprite.filters.internal.addX()` (object-local, cheaper) or `sprite.filters.external.addX()` (screen-space). e.g. `sprite.preFX.addGlow()` becomes `sprite.enableFilters(); sprite.filters.internal.addGlow()`. +- `setPipeline('Light2D')` is removed → `sprite.setLighting(true)` (per-object Lighting component; optional `setSelfShadow(...)`). Custom WebGL `Pipeline`s are gone too (replaced internally by RenderNodes) — do not hand-write a v3 pipeline; use Filters for post-processing. +- WebGL masks: `new Phaser.Display.Masks.BitmapMask(...)` + `setMask()` are removed for WebGL → `sprite.enableFilters(); sprite.filters.internal.addMask(maskTextureOrObject)`. (GeometryMask survives on the Canvas renderer only.) +- `Phaser.Geom.Point` is removed → use `Phaser.Math.Vector2` or a plain `{ x, y }` object. +- `Phaser.Structs.Set` is removed → use native JS `Set` (`add`/`has`/`delete`/`forEach`). (`Phaser.Structs.Map` still exists in v4, but native `Map` is fine for plain key/value storage.) +- `Phaser.Math.PI2` is removed → `Phaser.Math.TAU` (NOTE: in v4 `TAU` equals 2π, not π/2; for π/2 use `Phaser.Math.PI_OVER_2`). +- `RenderTexture` / `DynamicTexture` drawing is now buffered — `draw`/`stamp`/`fill`/`erase` push commands and you MUST call `.render()` to flush them (or set `renderMode: 'all'`); they no longer paint immediately. +- There is no dedicated Bloom FX → use `Phaser.Actions.AddEffectBloom(...)` or ParallelFilters (Threshold + Blur + `ADD` blend). +- `Phaser.Create.GenerateTexture` and the Create palettes are removed → build textures with a Graphics object + `generateTexture(key, w, h)` (the Graphics method still exists; the Create utility does not). +- `roundPixels` now defaults to `false` (v3 defaulted to `true`) — set it explicitly in the game/camera config when you need crisp pixel art. + +When tempted to reach for a v3-era API not listed here, prefer the documented v4 component/method and do not assume a v3 signature. +</v3_apis_removed_use_v4> + +<v4_capabilities_to_use> +v4 adds GPU-accelerated systems worth reaching for. One-liners below; full usage rides the relevant <phaser_instructions> section when the topic comes up: +- Filters (`filters.internal` / `filters.external` after `enableFilters()`): modern post-processing on any object or camera — glow, blur, bloom, color matrix, displacement, vignette, masks. See filters_and_postfx. +- Lighting + self-shadow (`setLighting(true)`, `setSelfShadow(...)`): cheap 2D lighting with no pipeline. See game_object_components. +- New tint modes (`setTintMode(Phaser.TintModes.ADD | SCREEN | OVERLAY | HARD_LIGHT)`): additive glows and blends without filters. +- SpriteGPULayer (`this.add.spriteGPULayer(...)`): renders huge numbers of quads (up to millions) in a single draw call — reach for it on bullet-hell, dense particles-as-sprites, forests, starfields. See sprites_and_images. +- TilemapGPULayer (create the layer with the GPU option enabled): GPU tilemap whose cost is per-pixel, not per-tile, with no seams in LINEAR filtering. See tilemaps. +- Gradient and Noise game objects (`this.add.gradient(...)`, `this.add.noiseSimplex2D(...)`, etc.): GPU-generated gradients and procedural noise for skies, fog, water, and fire. See graphics_and_shapes / render_textures. +- CaptureFrame (`this.add.captureFrame(key)`): snapshot the framebuffer at a point in the display list into a texture, for echo/feedback effects. +</v4_capabilities_to_use> +</phaser_v4_guide> diff --git a/skills/base44-platform-games/references/game-3d.md b/skills/base44-platform-games/references/game-3d.md new file mode 100644 index 0000000..95c5159 --- /dev/null +++ b/skills/base44-platform-games/references/game-3d.md @@ -0,0 +1,149 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: 3D game (Three.js): architecture, assets, analytics and the Three.js reference + +<game_architecture> +The game project is a standard React/Base44 app with a game layer under `src/game/`. + +<file_structure> +The game template ships this skeleton ALREADY WIRED: `src/App.jsx` routes `/` to GamePage, `GamePage.jsx` renders `<GameContainer />` (the engine-agnostic mount that owns the `#game-container` div), and `src/game/main.js` re-exports the active engine flavor. Build the game by EDITING these files in place — never recreate them or build a parallel structure elsewhere. The skeleton files are small and self-documenting: read them before changing them instead of relying on a from-memory picture of their contents. + +- `src/game/3d/` - ALL gameplay code for this game: scenes, game objects, systems, spawning, scoring, enemy AI. The starter scene is `scenes/GameScene.jsx` — gameplay code starts THERE; add further scene files under `scenes/` and reusable game-object files (e.g. `objects/`) alongside it. +- `src/game/main.js` - one-line engine switch re-exporting the active flavor's `Game` component; must point at `@/game/3d/Game` for this game. +- `src/game/config.js` - create for tunable game constants (see configuration). +- `src/game/EventBus.js` - engine-agnostic event emitter for React <-> game communication; rarely needs editing. +- `src/pages/GamePage.jsx` - React page that mounts the game; edit to build the HUD/menu overlay. +- `src/components/game/GameContainer.jsx` - mount wrapper; rarely needs editing. +- `src/App.jsx` - React router and Base44 shell; already mounts GamePage at `/` — edit only to add MORE routes. +- `src/main.jsx` - React bootstrap. Do not edit this for game routing. + +An older game may have a flatter layout — no `2d/`/`3d/` flavor split, a `PhaserGame`/`ThreeGame` mount instead of `GameContainer`. Follow the existing wiring in that case; the placement and boundary rules below still apply. + +Placement rule: ALL gameplay code lives under `src/game/`. `src/components/game/` holds ONLY the mount wrapper. React overlay UI components go in `src/components/` (or inline in GamePage.jsx) and contain no gameplay logic. +</file_structure> + +<boundary_rules> +- Put Base44 SDK calls, auth-aware data loading, navigation, and ALL screen-space UI — start/pause/settings menus, game-over and victory screens, score/lives/level readouts, buttons, dialogs, leaderboards, forms — in React files as a DOM overlay over the canvas (see screen_space_ui below). Do not build these inside the game world. +- Put gameplay state, physics, collisions, animation, game input, cameras, render-loop behavior, and world-anchored text (floating damage numbers, speech bubbles above characters, pickup labels) in `src/game`. React overlay state is a display copy fed by events — never the authoritative home for gameplay data. +- Use `src/game/EventBus.js` for ALL React <-> game communication, in both directions: the game emits gameplay events (score changed, game over) that React UI renders; React emits commands (restart, pause, resume) that the game acts on. +- Persist durable game data (high scores, saves, progress, leaderboards) through Base44 entities from React — never call the SDK inside the game loop or a scene; bridge gameplay events to React via `EventBus`. +- Preserve the `current-scene-ready` event pattern: the active scene emits it once it is mounted so `GameContainer` can track its handle. This event does NOT mean the first playable frame rendered and must not drive `window.game.ready`. + +</boundary_rules> + +<screen_space_ui> +Decision rule: if an element would look and behave correctly as a DOM element positioned over the canvas with CSS, build it in React (Tailwind + shadcn/ui). If it must track a world position, move with the camera, or update every frame in lockstep with rendering, build it in the game world. +- React overlay: menus, pause/game-over/victory screens, score-lives-level HUD readouts, buttons, settings, instructions, dialogs, leaderboards, forms. +- Game world: damage numbers, world-anchored labels and speech bubbles, and per-frame readouts tightly coupled to the render loop. + +Overlay structure: overlay elements are SIBLINGS of `<GameContainer />` inside the game frame wrapper (`.game-frame` is already `position: relative`). The overlay layer is `absolute inset-0 pointer-events-none` so gameplay clicks reach the canvas; interactive children re-enable `pointer-events-auto`: +```jsx +<div className="game-frame"> + <GameContainer /> + <div className="absolute inset-0 pointer-events-none"> + <div className="absolute top-3 left-3 text-white text-base sm:text-xl font-bold">Score: {score}</div> + {phase === 'game-over' && ( + <div className="absolute inset-0 grid place-items-center bg-black/60 pointer-events-auto"> + <Button onClick={() => EventBus.emit('restart-game')}>Play Again</Button> + </div> + )} + </div> +</div> +``` + +Two-way EventBus contract: +- Game -> React (state): emit on DISCRETE gameplay events only — `EventBus.emit('score-changed', score)` when the score actually changes, `EventBus.emit('game-over', { score })` once. NEVER emit from the per-frame update loop to drive React state; a value that changes every frame stays rendered in the game world, or its emit is throttled (e.g. every 100-250ms). +- Ticking overlay state (run timers, cooldown readouts — anything on an interval or throttled emit) lives in a LEAF component that owns its own state, never in the page component: overlay components are React ancestors of the game mount, so state there re-renders the Canvas/game subtree every tick and the whole scene reconciles at that rate. +- React -> Game (commands): the scene/game component registers listeners in a `useEffect` and removes them in its cleanup — EventBus outlives remounts, so leftover listeners stack up. +- React side: subscribe in `useEffect` with stable named handlers and matching `EventBus.off` cleanup. + +Overlay fonts: the template maps `font-display`, `font-heading`, `font-body`, and `font-mono` to `--font-*` CSS variables — to use a custom font, add `@import url('...')` at the very top of `src/index.css` (before `@tailwind base;`) and change the matching `--font-*` variable in `:root`. Never `@apply` a class that is not a built-in Tailwind utility or one mapped in `tailwind.config.js` — the build fails with "The `X` class does not exist". +Overlay color token syntax must match its consumer: `hsl(var(--token))` requires HSL channels such as `0 0% 100%`; an RGB tuple such as `248 250 252` requires `rgb(var(--token))`. Never put unitless RGB channels into an HSL token. +</screen_space_ui> + +<import_path_rules> +Import app and game modules with the `@/` alias rooted at `src/`. This is the style the skeleton files already use — mirror it in every file you create or edit. + +- Examples: `import { EventBus } from '@/game/EventBus';`, `import { GameScene } from '@/game/3d/scenes/GameScene';`, `import { GameContainer } from '@/components/game/GameContainer';`, `import { SHOP_ITEMS } from '@/game/config';`, `import { base44 } from '@/api/base44Client';`. +- A relative import (`./sibling`) is fine for a file in the same directory, but prefer `@/` for anything that crosses directories. +- Never import app modules with bare paths like `game/EventBus` or `components/game/GameContainer` — the build treats those as npm packages and fails to resolve them. +</import_path_rules> + +<configuration> +For non-trivial games, create `src/game/config.js` and keep tunable game constants there: +- dimensions, speeds, gravity, score values, colors, asset keys, art style, and perspective. +- Import constants from `src/game/config.js` into scenes and the engine setup. +- When the user asks to tune behavior ("faster", "more gravity", "larger world"), prefer editing config constants instead of scattering magic numbers. +</configuration> + +<testability> +- Every player- or AI-initiated gameplay capability must be callable independently of rendering and its physical input binding. Human-input and AI handlers are thin wrappers over the same gameplay functions. Represent sustained or analog controls with explicit control-state setters (press/release or normalized values); never add verification-only movement that bypasses the real update, timing, or physics path. Do not write the tests themselves +- Route all gameplay randomness through one seedable RNG (a small seeded generator in `src/game`). Vary the seed per run; a pinned seed is for reproducible verification. +- Expose and MAINTAIN the game API on `window.game`: `ready`, side-effect-free state getters (phase, score, key entities), and the capability/control functions required to exercise the primary playable loop. Initialize/reset `ready` to false before boot and restart; set it true only after required assets/async systems have finished and at least one playable frame rendered. The readiness signal belongs inside the final Suspense/physics boundary and completes after that world participates in its first render frame. Keep the API current as the game grows — a stale API is worse than none. +- HUD and menus stay in the React DOM overlay (see screen_space_ui) — that is the automated-verifiable surface. +</testability> +</game_architecture> + +<game_assets> +By default, build game visuals programmatically in code with Three.js meshes, materials, and lights; for richer visuals than code shapes can produce (detailed characters, creatures, props, tiles, full backgrounds), generate images with the runtime `GenerateImage` integration or use assets the user provides, describing the project's `ART_STYLE` in every prompt so assets share one look, and store each image URL in `src/game/config.js` to load as textures — reference the saved URL on every run rather than regenerating on each build. + +Composing the 3D world itself (geometry, materials, lighting, procedural textures) is covered in the Three.js reference. + +<style_consistency> +Pick one cohesive look before building: a vibrant color palette and a shape language +(e.g. neon, blocky), and keep it consistent across the whole world. Store the +choices in `src/game/config.js` (e.g. `PALETTE`, `ART_STYLE`, `CAMERA_MODE`) when +that file exists or when you create it, and read from it everywhere. + +The camera mode shapes how you build and light the world — first-person, third-person +follow, orbit, or a fixed angle. Settle it up front and keep it consistent. +</style_consistency> + +Do not mix inconsistent visual styles in the same scene unless the user explicitly wants that contrast. +</game_assets> + +To opt-out, set `{"analytics":{"enabled":false}}` when initializing the base44 SDK (supported from 0.8.46). + +<threejs_reference> +This game is 3D: the world runs on Three.js through React Three Fiber (R3F) — there is NO Phaser in this game. The stack: +- `three` — the engine (always installed). +- `@react-three/fiber` — renders the Three.js scene graph as React components and owns the canvas and render loop. +- `@react-three/drei` — ready-made helpers (controls, cameras, loaders, text, sky, ...). Reach for drei before hand-rolling a helper. +- `@react-three/rapier` — physics (`<Physics>`, `<RigidBody>`, colliders). + +<dependencies> +Never install or re-pin a package already in `package.json`, and never import a package that is not in it. +</dependencies> + +<legacy_3d_games> +An older 3D game may mount Three.js manually — a component owning a `WebGLRenderer` and a `requestAnimationFrame` loop — instead of R3F. Follow that existing structure when editing such a game; do not rewrite it onto R3F unless the user asks. +</legacy_3d_games> + +<r3f_essentials> +- Scene content is JSX: `<mesh position={[0, 1, 0]}><boxGeometry args={[1, 1, 1]} /><meshStandardMaterial color="#38bdf8" /></mesh>`. `args` are constructor arguments; nested properties use dashed "pierced" props (`shadow-mapSize`, `shadow-camera-left`, `position-x`). +- Per-frame gameplay runs in `useFrame((state, delta) => ...)` inside components under the Canvas. Scale all movement by `delta` (seconds) so speed is frame-rate independent. +- NEVER set React state per frame, and NEVER hold live gameplay data — entity arrays (enemies, projectiles), hp, cooldowns — in useState: `useFrame` closures capture the render-time snapshot, so systems iterate a stale list and hits/targeting silently break. Mutate refs and shared module objects in `useFrame` (`meshRef.current.position.x += speed * delta`); React state only mirrors discrete events (phase changes, score). +- `useThree` gives the root state (scene, camera, gl, size). R3F hooks (`useFrame`, `useThree`, `useTexture`) only work in components rendered inside the Canvas — the React overlay outside cannot use them. +- Anything that loads asynchronously (textures, models) suspends — wrap it in `<Suspense fallback={null}>`. +- Structure the world as small components under `src/game/3d/` (Player, Enemies, Level, Effects) composed in the scene; keep tunables in `src/game/config.js`. +</r3f_essentials> + +<coordinates> +- The three.js frame: +X right, +Y up, and a camera looks down its −Z axis — "forward" / into the screen is −Z; +Z points at the viewer. +- The control convention is DERIVED from the camera rig and control scheme settled for this game, never assumed — record it once in `src/game/config.js` as signed constants the gameplay formulas import (forward/flow sign, spawn distance, camera offset), not just a `CAMERA_MODE` label: labels document, only consumed numbers keep files in agreement. A file that invents its own sign inline instead of importing it is how one subsystem ships mirrored against the rest. Chase/first-person (camera yaw follows the avatar): avatar faces −Z, camera behind at a positive local-Z offset, input camera-relative. Fixed rig (camera yaw never changes — top-down, isometric, side-view, lane runner): keys map to the camera's screen axes projected onto the ground, fixed world-direction constants. Aim/pointer games (pool, golf, board/puzzle): there may be no movement keys at all — don't bolt WASD on. +- Every system must derive from that one convention — input signs, model orientation, camera offset, spawn/flow direction, cull windows. A single flipped sign in one file reads as mirrored controls or a world scrolling backwards; fix a wrong direction at its source instead of negating it downstream. +</coordinates> + +<physics_rapier> +Use rapier when the game needs physical simulation — gravity, stacking, bouncing, knock-back, vehicles. Grid/board/puzzle games and simple arcade motion move meshes directly in `useFrame` without `<Physics>`. Wrap the physical world in `<Physics>` inside `<Suspense>` (the rapier WASM module initializes asynchronously); bodies, colliders, forces, and collision/trigger events are covered in the injected `physics_dynamics` and `triggers_and_collisions` reference sections. +</physics_rapier> + +<drei_helpers> +Common picks: `OrbitControls` (add `makeDefault`), `PerspectiveCamera`/`OrthographicCamera`, `KeyboardControls`, `Text`/`Billboard`, `Sky`, `Stars`, `Environment`, `useTexture`, `Html` — each is detailed in its injected reference section. +</drei_helpers> + +<visuals> +Compose visuals from primitives (`boxGeometry`, `sphereGeometry`, `cylinderGeometry`, `coneGeometry`, `planeGeometry`, `torusGeometry`, `capsuleGeometry`) inside `<group>`s for characters and props; share geometries/materials and vary color, scale, and rotation per instance. Image-tool URLs load as textures via `useTexture(url)` — art direction, palette, and asset rules are in the game assets guidance; lighting, shadows, and atmosphere in the injected `scene_and_rendering` reference section. +</visuals> +</threejs_reference> diff --git a/skills/base44-platform-games/references/phaser/actions_and_utilities.md b/skills/base44-platform-games/references/phaser/actions_and_utilities.md new file mode 100644 index 0000000..6d5170f --- /dev/null +++ b/skills/base44-platform-games/references/phaser/actions_and_utilities.md @@ -0,0 +1,118 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: actions and utilities + +<actions_and_utilities> +Use this when you need batch operations on arrays of Game Objects, alignment and grid layout, or the Phaser.Utils array/object/string helper functions. + +<quick_start> +`Phaser.Actions` runs batch operations over arrays of Game Objects. The first argument is always an array, every action returns the same array, and Groups work by passing `group.getChildren()`. + +```js +// Create 20 sprites and batch-position them in a grid +const sprites = []; +for (let i = 0; i < 20; i++) { + sprites.push(this.add.sprite(0, 0, 'gem')); +} + +// Arrange into a 5x4 grid +Phaser.Actions.GridAlign(sprites, { + width: 5, + height: 4, + cellWidth: 64, + cellHeight: 64, + x: 100, + y: 100 +}); + +// Fade alpha from 0 to 1 across all sprites +Phaser.Actions.Spread(sprites, 'alpha', 0, 1); + +// Offset each sprite's x by 10, with a step of 2 per item +Phaser.Actions.IncX(sprites, 10, 2); + +// Works with Groups too +const group = this.add.group({ key: 'star', repeat: 11 }); +Phaser.Actions.PlaceOnCircle(group.getChildren(), new Phaser.Geom.Circle(400, 300, 200)); +``` +</quick_start> + +<core_concepts> +Every Action in `Phaser.Actions` follows the same shape: the first argument is an array of Game Objects (or any objects with the required public properties like `x`, `y`, `alpha`); it returns the same array for chaining/pass-through; it works with Groups via `group.getChildren()`; and it is a one-shot batch operation that stores no state. + +Most Set/Inc actions delegate to two core functions. `PropertyValueSet(items, key, value, step, index, direction)` sets `items[i][key] = value + (i * step)`. `PropertyValueInc(items, key, value, step, index, direction)` adds `items[i][key] += value + (i * step)`. The `step` parameter adds an incremental offset per item (multiplied by iteration index, not cumulative). The `index` and `direction` parameters control the iteration start point and order (1 = forward, -1 = backward). + +Placement actions like `PlaceOnCircle`, `PlaceOnLine`, `RandomRectangle` accept Phaser geometry objects (`Phaser.Geom.Circle`, `Phaser.Geom.Line`), NOT Game Object shapes. If using a `Phaser.GameObjects.Circle`, pass its `.geom` property instead. + +Action categories: property setters (`SetX`, `SetXY`, `SetAlpha`, `SetScale`, `SetTint`, `SetVisible`, generic `PropertyValueSet`); property incrementers (`IncX`, `IncXY`, `Angle`, `Rotate`, `ScaleXY`, generic `PropertyValueInc`); placement on geometry (`PlaceOnCircle/Ellipse/Line/Rectangle/Triangle`, `RandomCircle/Ellipse/Line/Rectangle/Triangle`); layout and alignment (`GridAlign`, `AlignTo`, `FitToRegion`); distribution (`Spread`, `SmoothStep`, `SmootherStep`); rotation and movement (`RotateAround`, `RotateAroundDistance`, `ShiftPosition` for snake-like trailing, `WrapInRectangle`); queries and iteration (`GetFirst`, `GetLast`, `Call`, `Shuffle`, `ToggleVisible`, `PlayAnimation`); and v4 filter effects (`AddEffectBloom`, `AddEffectShine`, `AddMaskShape`, which return arrays of created effects rather than the input array). + +`Phaser.Utils.Array` offers array manipulation: `Add`, `AddAt`, `Remove`, `RemoveAt`, `RemoveBetween`, `RemoveRandomElement`, `Replace`, `Swap`, `BringToTop`, `SendToBack`, `MoveUp`, `MoveDown`, `MoveAbove`, `MoveBelow`, `MoveTo`, `RotateLeft`, `RotateRight`, `Each`, `EachInRange`, `GetAll`, `GetFirst`, `GetRandom`, `CountAllMatching`, `SetAll`, `Shuffle`, `StableSort`, `SortByDigits`, `QuickSelect`, `FindClosestInSorted`, `Flatten`, `NumberArray`, `NumberArrayStep`, `Range`, `SafeRange`, `SpliceOne`. There is also a `Phaser.Utils.Array.Matrix` sub-namespace for 2D matrix operations. + +`Phaser.Utils.Objects` offers object helpers: `Clone` (shallow), `DeepCopy` (recursive), `Extend` (jQuery-style), `Merge`/`MergeRight`, `Pick`, `GetValue` (dot-path lookup with fallback), `GetFastValue` (top-level-only, faster), `GetAdvancedValue` (resolves random/callback config values), `GetMinMaxValue` (clamped), `SetValue` (dot-path set), `HasValue`, `HasAll`, `HasAny`, `IsPlainObject`. + +`Phaser.Utils.String` offers: `Format` (replace `%1`, `%2` markers), `Pad`, `RemoveAt`, `Reverse`, `UppercaseFirst`, `UUID` (RFC4122 v4). +</core_concepts> + +<common_patterns> +Using Actions with Groups — always pass the array, not the Group: +```js +const enemies = this.add.group({ key: 'enemy', repeat: 9 }); + +// Position all children in a grid +Phaser.Actions.GridAlign(enemies.getChildren(), { + width: 5, height: 2, + cellWidth: 80, cellHeight: 80, + x: 100, y: 50 +}); + +// Fan out alpha across all enemies +Phaser.Actions.Spread(enemies.getChildren(), 'alpha', 0.3, 1); +``` + +The step parameter for staggered values: +```js +// Place sprites starting at x=100, each one 50px further right +Phaser.Actions.SetX(sprites, 100, 50); +// Result: items[0].x=100, items[1].x=150, items[2].x=200, ... + +// Increment rotation with increasing step +Phaser.Actions.Rotate(sprites, 0.1, 0.05); +// Result: items[0].rotation += 0.1, items[1].rotation += 0.15, ... +``` + +Scatter then constrain: +```js +const bounds = new Phaser.Geom.Rectangle(0, 0, 800, 600); +Phaser.Actions.RandomRectangle(sprites, bounds); // random scatter +Phaser.Actions.WrapInRectangle(sprites, bounds); // keep wrapped in bounds during update +``` + +`GetValue` for config parsing: +```js +// Deep property access with fallback +const width = Phaser.Utils.Objects.GetValue(config, 'render.screen.width', 800); + +// Fast top-level lookup (no dot-path, better performance) +const speed = Phaser.Utils.Objects.GetFastValue(config, 'speed', 100); +``` + +`NumberArray` for asset keys: +```js +const keys = Phaser.Utils.Array.NumberArray(1, 10, 'frame_', '.png'); +// Result: ['frame_1.png', 'frame_2.png', ..., 'frame_10.png'] +``` +</common_patterns> + +<gotchas> +- Actions operate on plain arrays, not Groups directly. Always call `group.getChildren()` to get the array. +- PlaceOn/Random geometry actions need `Phaser.Geom` objects, not Game Object shapes. For a `Phaser.GameObjects.Circle`, pass `circle.geom`. +- `SetXY` defaults `y` to `x` if `y` is `undefined` or `null`. Pass `0` explicitly if you want y=0 and x=something else. +- `Spread` with a single item places it at the midpoint `(min + max) / 2`, not at `min`. +- `step` is multiplied by iteration index, not added cumulatively. Item 0 gets `value + 0*step`, item 1 gets `value + 1*step`, etc. +- `StableSort` uses native `Array.sort` when the engine supports stable sorting, falling back to merge sort only when needed. +- `Shuffle` (both Actions and Utils.Array) modifies the array in-place and returns it. +- `GetValue` vs `GetFastValue`: use `GetFastValue` when the key is always top-level (no dots) — it skips dot-path parsing and is faster in hot loops. +- `AddEffectBloom` / `AddEffectShine` / `AddMaskShape` are v4-only filter-based effects. They return arrays of created effects instead of the input. +</gotchas> +</actions_and_utilities> diff --git a/skills/base44-platform-games/references/phaser/animations.md b/skills/base44-platform-games/references/phaser/animations.md new file mode 100644 index 0000000..5cbea05 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/animations.md @@ -0,0 +1,249 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: animations + +<animations> +Use this when creating or controlling sprite animations in Phaser 4: spritesheets, atlases, the AnimationManager and AnimationState, play/stop/chain, frame callbacks, and animation events. + +<quick_start> +Load a spritesheet in `preload`, define a global animation in `create` via `this.anims.create`, then play it on a sprite. + +```js +// In preload -- load a spritesheet +this.load.spritesheet('explosion', 'explosion.png', { + frameWidth: 64, + frameHeight: 64 +}); + +// In create -- define a global animation +this.anims.create({ + key: 'explode', + frames: this.anims.generateFrameNumbers('explosion', { start: 0, end: 11 }), + frameRate: 24, + repeat: 0 +}); + +// Play it on a sprite +const sprite = this.add.sprite(400, 300, 'explosion'); +sprite.play('explode'); +``` +</quick_start> + +<core_concepts> +Phaser has two distinct animation objects. The **AnimationManager** (`this.anims` in a Scene, or `this.game.anims`) is a global singleton owned by the Game — it creates and stores animation definitions, available across every Scene. The **AnimationState** (`sprite.anims`) is a per-sprite instance that controls playback on one Game Object. An `Animation` is a sequence of `AnimationFrame` objects plus timing data, created via `this.anims.create(config)` (global) or `sprite.anims.create(config)` (local to one sprite). + +When `sprite.anims.play(key)` is called, it first checks for a local animation with that key, then falls back to the global AnimationManager — local takes priority. Use local for sprite-specific animations; use global when shared across sprites. + +```js +// Global animation -- available to all sprites +this.anims.create({ key: 'walk', frames: 'player_walk', frameRate: 12, repeat: -1 }); + +// Local animation -- only on this sprite +sprite.anims.create({ key: 'walk', frames: 'npc_walk', frameRate: 10, repeat: -1 }); + +// This plays the LOCAL version because local takes priority +sprite.play('walk'); +``` + +Duration vs frameRate priority: if both are null it defaults to 24 fps; if only `duration` is set, frameRate is calculated as `totalFrames / (duration / 1000)`; if `frameRate` is set (even alongside duration), frameRate wins and duration is derived. Sprite shorthand methods `sprite.play()`, `sprite.playReverse()`, `sprite.chain()`, `sprite.stop()` wrap their `sprite.anims.*` equivalents. +</core_concepts> + +<common_patterns> +Spritesheet animation uses `generateFrameNumbers` (numeric frame indices). Config keys: `start` (default 0), `end` (default -1 = last frame), `first` (single frame to prepend), `frames` (explicit array, overrides start/end). +```js +this.load.spritesheet('dude', 'dude.png', { frameWidth: 32, frameHeight: 48 }); + +// All frames +this.anims.create({ + key: 'run', + frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 7 }), + frameRate: 10, + repeat: -1 +}); + +// Custom frame sequence +this.anims.create({ + key: 'idle', + frames: this.anims.generateFrameNumbers('dude', { frames: [0, 1, 2, 1] }), + frameRate: 6, + repeat: -1 +}); +``` + +Atlas animation uses `generateFrameNames` (string frame names). Config keys: `prefix`, `suffix`, `start`, `end`, `zeroPad` (left-pad numbers to length with zeros), `frames` (explicit, overrides range). Calling `generateFrameNames(key)` with no config returns all frames. +```js +this.load.atlas('gems', 'gems.png', 'gems.json'); + +this.anims.create({ + key: 'ruby_sparkle', + frames: this.anims.generateFrameNames('gems', { + prefix: 'ruby_', + start: 1, + end: 6, + zeroPad: 4 // produces ruby_0001 through ruby_0006 + }), + frameRate: 12, + repeat: -1 +}); +``` + +Pass a texture key string as `frames` to use all frames from that texture, sorted numerically by default (`sortFrames: false` disables sorting): +```js +this.anims.create({ key: 'walk', frames: 'player_walk', frameRate: 12, repeat: -1 }); +``` + +Yoyo and repeat — when `yoyo` is true the animation plays forward then reverses, and the full cycle counts as one play. `repeat: -1` means forever. +```js +this.anims.create({ + key: 'pulse', + frames: this.anims.generateFrameNumbers('orb', { start: 0, end: 5 }), + frameRate: 10, + yoyo: true, + repeat: -1, + repeatDelay: 500 // ms pause between each repeat cycle +}); +``` + +Chaining is per-sprite — chained animations start after `animationcomplete` or `animationstop`. An animation with `repeat: -1` never completes, so call `stop()` to trigger the chain. +```js +sprite.play('attack'); +sprite.chain('idle'); // play idle after attack completes +sprite.chain(['fall', 'land', 'idle']); // chain multiple +sprite.anims.chain(); // clear the chain queue +``` + +Reverse playback: `sprite.playReverse('walk')` sets `forward = false`/`inReverse = true`; `sprite.anims.reverse()` toggles direction mid-playback. + +Play variants: +```js +sprite.play('walk', true); // ignoreIfPlaying = true +sprite.anims.playAfterDelay('walk', 1000); // play after 1s delay +sprite.anims.playAfterRepeat('walk', 2); // play after current anim repeats 2x +``` + +Animation mixing adds a transition delay between two specific animations, set globally on the manager. Mixes apply only with `sprite.play()`, not `playAfterDelay`/`playAfterRepeat`. +```js +this.anims.addMix('idle', 'walk', 200); +this.anims.addMix('walk', 'idle', 300); + +sprite.play('idle'); +sprite.play('walk'); // 200ms mix delay applied automatically + +this.anims.removeMix('idle', 'walk'); // remove specific pair +this.anims.removeMix('idle'); // remove all mixes for 'idle' +``` + +Pause, resume, and stop. All stop methods fire `animationstop` (not `animationcomplete`), and chained animations trigger after stop. +```js +sprite.anims.pause(); // pause per-sprite +sprite.anims.resume(); // resume per-sprite +this.anims.pauseAll(); // global pause +this.anims.resumeAll(); // global resume + +sprite.anims.stop(); // stop immediately +sprite.anims.stopAfterDelay(2000); // stop after 2 seconds +sprite.anims.stopAfterRepeat(1); // stop after 1 more repeat +sprite.anims.stopOnFrame(frame); // stop when a specific frame is reached +``` + +Animation events all share the signature `(animation, frame, gameObject, frameKey)`. Available: `animationstart`, `animationcomplete`, `animationcomplete-{key}`, `animationupdate`, `animationstop`, `animationrepeat`, `animationrestart`. Manager-level events on `this.anims`: `addanimation`, `removeanimation`, `pauseall`, `resumeall`. +```js +sprite.on('animationcomplete', (anim, frame, gameObject, frameKey) => { + console.log('completed:', anim.key); +}); + +// Key-specific complete -- only fires for the named animation +sprite.on('animationcomplete-explode', (anim, frame, gameObject, frameKey) => { + gameObject.destroy(); +}); + +// Frame-level callback via animationupdate +sprite.on('animationupdate', (anim, frame, gameObject, frameKey) => { + if (anim.key === 'attack' && frame.index === 4) { + this.checkHit(gameObject); + } +}); +``` + +Per-frame duration (ms) is added on top of the base msPerFrame: +```js +this.anims.create({ + key: 'combo', + frames: [ + { key: 'fighter', frame: 'punch1', duration: 50 }, + { key: 'fighter', frame: 'kick', duration: 200 }, // hold longer + { key: 'fighter', frame: 'recover', duration: 100 } + ], + frameRate: 24 +}); +``` + +Visibility, random start, and timeScale: +```js +this.anims.create({ + key: 'appear', frames: 'sparkle', frameRate: 12, + showOnStart: true, // sprite.visible = true when anim starts (after delay) + hideOnComplete: true, // sprite.visible = false when anim completes + showBeforeDelay: true // show first frame immediately even during delay +}); + +this.anims.create({ + key: 'ambient', frames: 'fire', frameRate: 10, repeat: -1, + randomFrame: true // each sprite begins on a different frame +}); + +sprite.anims.timeScale = 2; // 2x speed +sprite.play({ key: 'walk', timeScale: 0.5 }); // half speed via config +this.anims.globalTimeScale = 0.5; // affects ALL animations +``` + +Staggered playback across a group — each sprite starts after the previous (pass `staggerFirst: false` to skip the delay on the first): +```js +const enemies = this.add.group({ key: 'enemy', repeat: 9 }); +this.anims.staggerPlay('walk', enemies.getChildren(), 100); +``` + +JSON export/import and existence checks: +```js +const data = this.anims.toJSON(); +this.anims.fromJSON(data); // import +this.anims.fromJSON(data, true); // pass true to clear existing first + +if (!this.anims.exists('walk')) { + this.anims.create({ key: 'walk', frames: 'player_walk', frameRate: 12, repeat: -1 }); +} +``` + +Modifying animation frames at runtime: +```js +const anim = this.anims.get('walk'); +anim.addFrame(this.anims.generateFrameNumbers('player', { start: 8, end: 10 })); // append +anim.addFrameAt(this.anims.generateFrameNumbers('player', { frames: [5] }), 2); // insert at index +anim.removeFrame(anim.frames[3]); // remove a frame object +anim.removeFrameAt(0); // remove frame at index +``` + +Aseprite support creates animations from Aseprite tags: +```js +this.load.aseprite('paladin', 'paladin.png', 'paladin.json'); +// In create: +this.anims.createFromAseprite('paladin'); // all tags +this.anims.createFromAseprite('paladin', ['walk']); // specific tags only +sprite.play('walk'); // play by tag name +``` +</common_patterns> + +<gotchas> +1. Animations are global by default. `this.anims.create()` registers across all Scenes. Do not recreate in every Scene — it logs a warning and returns the existing one. +2. `repeat: -1` never fires `animationcomplete`. Use `stop()` to end infinite animations; listen for `animationstop` instead. +3. `frameRate` beats `duration`. If both are set, frameRate wins. Set only `duration` (leave frameRate null) to control total length. +4. Per-frame `duration` is additive — added on top of base msPerFrame, not a replacement. +5. `play()` stops the current animation (fires `animationstop`). Use `play(key, true)` to skip if already playing. +6. Mix delays only work with `play()`. `playAfterDelay`/`playAfterRepeat` bypass mixes. +7. Local animations override global. Same key on a sprite's local map takes priority. +8. Sprite shorthand methods (`sprite.play()`, `sprite.playReverse()`, `sprite.chain()`, `sprite.stop()`) wrap `sprite.anims.*`. +9. Chained anims fire after stop too. Clear the queue with `sprite.anims.chain()` before stopping if unwanted. +10. `generateFrameNumbers` end=-1 means last frame. The `__BASE` frame is excluded automatically. +</gotchas> +</animations> diff --git a/skills/base44-platform-games/references/phaser/audio_and_sound.md b/skills/base44-platform-games/references/phaser/audio_and_sound.md new file mode 100644 index 0000000..1a11c36 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/audio_and_sound.md @@ -0,0 +1,229 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: audio and sound + +<audio_and_sound> +Use this when adding audio or sound to a Phaser 4 game: loading audio, playing sounds and music, volume and panning, looping, markers, audio sprites, spatial audio, and the SoundManager. + +<quick_start> +`this.sound` is the SoundManager, available in any Scene. Use `play()` for fire-and-forget sounds or `add()` to retain a reference for ongoing control. Assets loaded via `this.load.audio()` in `preload()` are ready by the time `create()` runs; provide an array of URLs for cross-browser format fallback. + +```js +class GameScene extends Phaser.Scene { + preload() { + this.load.audio('bgm', 'assets/music.mp3'); + this.load.audio('coin', ['assets/coin.ogg', 'assets/coin.mp3']); + } + + create() { + // Fire-and-forget (auto-destroys when complete) + this.sound.play('coin'); + + // Retained reference for ongoing control + this.music = this.sound.add('bgm', { loop: true, volume: 0.5 }); + this.music.play(); + } +} +``` +</quick_start> + +<core_concepts> +Phaser auto-selects the best backend via `SoundManagerCreator.create()`. If `config.audio.noAudio` is true or the device supports neither backend, a **NoAudioSoundManager** is created (all calls are no-ops). If the device supports Web Audio and `disableWebAudio` is not true, a **WebAudioSoundManager** is created (preferred). Otherwise an **HTML5AudioSoundManager** is the fallback. Force a backend via game config: `audio: { disableWebAudio: true }` or `audio: { noAudio: true }`. Pass `audio: { context: existingAudioContext }` to reuse a WebAudio context in SPAs. + +WebAudio advantages: precise timing, gapless looping, stereo panning (`StereoPannerNode`), spatial audio (`PannerNode`), per-sound gain nodes, runtime `decodeAudio()`. HTML5 Audio limitations: no spatial audio, no real stereo panning (pan fires events but no audible effect), less precise looping, and it requires an `instances` count at load time for simultaneous playback. + +The SoundManager (`this.sound`) is a single shared instance across the entire game. It handles adding/playing/removing sound instances, global volume/mute/rate/detune, automatic pause/resume on tab blur (`pauseOnBlur`, default `true`), mobile audio unlocking, and the spatial audio listener position (WebAudio only). + +Sound instances are created via `this.sound.add(key, config)`. A sound must exist in the audio cache (loaded via the Loader) before it can be added. Each instance has its own playback state and `volume`, `rate`, `detune`, `loop`, `pan`, and `seek` properties. State flags: `isPlaying`, `isPaused`. + +```js +const sfx = this.sound.add('explosion', { volume: 0.8 }); +sfx.play(); // returns boolean +sfx.pause(); // only works if isPlaying +sfx.resume(); // only works if isPaused +sfx.stop(); // resets to stopped state +sfx.destroy(); // marks for removal from manager +``` +</core_concepts> + +<common_patterns> +Playing sounds — fire-and-forget (`this.sound.play(key, config?)` adds, plays, and auto-destroys on completion) or retained reference (`this.sound.add(key, config?)` then `play()`): +```js +this.sound.play('explosion'); +this.sound.play('powerup', { volume: 0.5, rate: 1.2 }); + +const laser = this.sound.add('laser'); +laser.play(); +// Later: laser.stop(), laser.volume = 0.3, etc. +``` + +Volume, rate, and detune — settable per-sound or globally on the manager; values combine (rate/detune multiply via `calculateRate()`). The effective playback rate is `sound.rate * manager.rate * detuneRate` where `detuneRate = Math.pow(1.0005777895065548, sound.detune + manager.detune)`. All `set*` methods return `this` for chaining. +```js +// Per-sound +sound.volume = 0.5; // 0 to 1 +sound.setVolume(0.5); // chainable +sound.rate = 1.5; // 0.5 = half speed, 2.0 = double speed +sound.detune = 200; // cents, -1200 to 1200 + +// Global (affects all sounds) +this.sound.volume = 0.8; +this.sound.rate = 1.0; +this.sound.detune = 0; +``` + +Looping — `LOOPED` fires each time the sound loops; `LOOP` fires when the loop property changes: +```js +const bgm = this.sound.add('music', { loop: true }); +bgm.play(); +bgm.loop = false; +bgm.setLoop(false); // chainable +``` + +Seeking — setting seek on a stopped sound has no effect: +```js +sound.seek = 5.0; // jump to 5 seconds in +sound.setSeek(5.0); // chainable +console.log(sound.seek); // current playback position in seconds +``` + +Stereo panning — uses `StereoPannerNode` on WebAudio; on HTML5 Audio the property fires events but has no audible effect: +```js +sound.pan = -1; // full left +sound.pan = 0; // center +sound.pan = 1; // full right +sound.setPan(0.5); // chainable +``` + +Audio sprites combine multiple sounds into one file with a JSON config (from the `audiosprite` tool); the JSON must be loaded separately. Its `spritemap` entries auto-convert to markers. You can also add markers manually to any sound via `addMarker(marker)`, `updateMarker(marker)`, `removeMarker(name)`. +```js +// In preload +this.load.audioSprite('sfx', 'assets/sfx.json', ['assets/sfx.ogg', 'assets/sfx.mp3']); + +// In create +this.sound.playAudioSprite('sfx', 'explosion'); +this.sound.playAudioSprite('sfx', 'coin', { volume: 0.5 }); + +// Or add for retained control +const sprite = this.sound.addAudioSprite('sfx'); +sprite.play('explosion'); + +// Manual markers on any sound +const sound = this.sound.add('longtrack'); +sound.addMarker({ name: 'intro', start: 0, duration: 5 }); +sound.addMarker({ name: 'loop', start: 5, duration: 20, config: { loop: true } }); +sound.play('intro'); +sound.play('loop'); // later +``` + +Background music — `pauseOnBlur` (default `true`) auto-pauses all sounds when the tab loses focus: +```js +this.bgm = this.sound.add('theme', { loop: true, volume: 0.4 }); +this.bgm.play(); +// Stop on scene shutdown: this.bgm.stop(); +``` + +Spatial audio (WebAudio only) uses a `PannerNode` to position sounds relative to a listener. Set `sound.x`/`sound.y` directly to reposition at any time; if `follow` is set, the position updates each frame. `setListenerPosition()` with no arguments defaults to the canvas center. +```js +this.sound.setListenerPosition(400, 300); +// Or: this.sound.listenerPosition.set(x, y); + +const enemy = this.sound.add('roar', { + source: { + x: 800, y: 300, + refDistance: 50, maxDistance: 2000, rolloffFactor: 1, + distanceModel: 'inverse', panningModel: 'equalpower', + follow: enemySprite // auto-track a Game Object's x/y + } +}); +enemy.play(); +``` + +Muting (per-sound or global): +```js +sound.mute = true; sound.setMute(true); +this.sound.mute = true; this.sound.setMute(true); +``` + +Querying sounds: +```js +this.sound.get('coin'); // first sound with key, or null +this.sound.getAll('coin'); // all sounds with key +this.sound.getAll(); // every sound in the manager +this.sound.getAllPlaying(); // all currently playing sounds +this.sound.isPlaying('coin'); // true if any 'coin' sound is playing +this.sound.isPlaying(); // true if any sound is playing +``` + +Removing and stopping: +```js +this.sound.stopAll(); // stop all, fires STOP_ALL +this.sound.stopByKey('coin'); // stop all with key, returns count +this.sound.pauseAll(); // fires PAUSE_ALL +this.sound.resumeAll(); // fires RESUME_ALL + +this.sound.remove(soundInstance); // destroy + remove specific sound +this.sound.removeByKey('coin'); // destroy + remove all with key, returns count +this.sound.removeAll(); // destroy + remove everything +``` + +Decoding audio at runtime (WebAudio only): +```js +this.sound.decodeAudio('key', base64StringOrArrayBuffer); +// Batch: this.sound.decodeAudio([{ key: 'sfx1', data: buf1 }, { key: 'sfx2', data: buf2 }]); +this.sound.on('decoded', (key) => { /* one done */ }); +this.sound.on('decodedall', () => { /* all done */ }); +``` +</common_patterns> + +<events> +Sound instance events (emitted on a Sound object): `PLAY` `'play'` `(sound)`, `PAUSE` `'pause'` `(sound)`, `RESUME` `'resume'` `(sound)`, `STOP` `'stop'` `(sound)`, `COMPLETE` `'complete'` `(sound)` (non-looping finish), `LOOPED` `'looped'` `(sound)`, `LOOP` `'loop'` `(sound, value)`, `MUTE` `'mute'` `(sound, value)`, `VOLUME` `'volume'`, `RATE` `'rate'`, `DETUNE` `'detune'`, `SEEK` `'seek'`, `PAN` `'pan'`, `DESTROY` `'destroy'` `(sound)`. + +SoundManager events (emitted on `this.sound`): `PAUSE_ALL` `'pauseall'`, `RESUME_ALL` `'resumeall'`, `STOP_ALL` `'stopall'` (each `(manager)`); `GLOBAL_MUTE` `'globalmute'`, `GLOBAL_VOLUME` `'globalvolume'`, `GLOBAL_RATE` `'globalrate'`, `GLOBAL_DETUNE` `'globaldetune'` (each `(manager, value)`); `UNLOCKED` `'unlocked'` `(manager)` (audio system unlocked after user interaction); `DECODED` `'decoded'` `(key)` and `DECODED_ALL` `'decodedall'` `()` (WebAudio decoding). + +```js +const sfx = this.sound.add('bang'); +sfx.on('complete', (sound) => { + console.log(sound.key, 'finished'); +}); +sfx.play(); +``` +</events> + +<gotchas> +Browser autoplay policy — browsers block audio until user interaction, and Phaser handles unlocking automatically. On WebAudio the `AudioContext` starts suspended and Phaser listens for touch/mouse/key events on `document.body` to call `context.resume()`; the `locked` property is true until unlocked and `UNLOCKED` fires once resolved. On HTML5 Audio, locked tags queue actions until the first touch. You do not need to handle unlocking manually, but you can wait for readiness: +```js +if (this.sound.locked) { + this.sound.once('unlocked', () => { this.sound.play('bgm'); }); +} else { + this.sound.play('bgm'); +} +``` + +Audio format support — no single format works everywhere. Provide multiple formats: `this.load.audio('bgm', ['assets/bgm.ogg', 'assets/bgm.mp3'])`. MP3 has broadest support; OGG Vorbis lacks Safari support; AAC/M4A works on Safari/iOS; WebM/Opus has excellent quality but limited older-browser support. + +HTML5 Audio simultaneous playback uses a pool of `<audio>` tags. Specify `instances` when loading for simultaneous playback: `this.load.audio('shot', 'assets/shot.mp3', { instances: 4 })` (default 1). If all tags are in use and `manager.override` is true (default), the sound with the most progress is hijacked. WebAudio has no such limit. + +iOS/Safari specifics — `StereoPannerNode` is unsupported, so `pan` has no audible effect (events still fire). iOS 17/18+ can interrupt audio on background; Phaser handles this via `context.suspend()`/`context.resume()` on the `VISIBLE` game event. `setListenerPosition()` and spatial audio are WebAudio-only. + +WebAudio context reuse — for SPAs that recreate the game without a full page reload, pass `audio: { context: existingAudioContext }` in config. You can also swap contexts at runtime via `this.sound.setAudioContext(newContext)` (WebAudio only). + +SoundManager is shared (global) — one per game, not per scene. Sounds are not automatically cleaned up on scene shutdown; you must stop/remove them yourself. Looping sounds continue across scene changes unless explicitly stopped. + +Fire-and-forget vs persistent — `this.sound.play(key)` creates a sound that auto-destroys on completion and cannot be controlled afterward. Use `this.sound.add(key)` when you need to pause, stop, adjust, or listen for events. + +Spatial audio is WebAudio only — the `source` config is silently ignored on HTML5 Audio. Do not rely on spatial positioning for gameplay-critical cues if HTML5 fallback must be supported. + +Web Audio Analyser (WebAudio only) — access the underlying `AudioContext` to create an `AnalyserNode` for frequency/waveform visualization. Check `this.sound.context` exists first. +```js +const analyser = this.sound.context.createAnalyser(); +analyser.fftSize = 256; +this.sound.masterVolumeNode.connect(analyser); +analyser.connect(this.sound.context.destination); + +const dataArray = new Uint8Array(analyser.frequencyBinCount); +// In update loop: +analyser.getByteFrequencyData(dataArray); // drive visual effects from dataArray +``` +</gotchas> +</audio_and_sound> diff --git a/skills/base44-platform-games/references/phaser/cameras.md b/skills/base44-platform-games/references/phaser/cameras.md new file mode 100644 index 0000000..8bdc6c3 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/cameras.md @@ -0,0 +1,250 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: cameras + +<cameras> +Use this when working with cameras in Phaser 4: the CameraManager and main camera, viewport vs scroll, zoom, bounds, following sprites, camera effects (fade, flash, shake, pan, zoomTo, rotateTo), ignore lists, filters, and keyboard controls. + +<quick_start> +Every Scene has a default camera at `this.cameras.main`. Access it in `create()` to scroll, zoom, follow, bound, and apply effects. + +```js +// In a Scene's create() method: + +// Access the default camera (created automatically) +const cam = this.cameras.main; + +// Scroll the camera to look at a different part of the world +cam.setScroll(200, 100); + +// Center camera on a world coordinate +cam.centerOn(400, 300); + +// Zoom in (2x) -- values < 1 zoom out, > 1 zoom in +cam.setZoom(2); + +// Follow a sprite with smooth lerp +cam.startFollow(player, false, 0.1, 0.1); + +// Constrain the camera to the world bounds +cam.setBounds(0, 0, 2048, 2048); + +// Fade in from black over 1 second +cam.fadeIn(1000); + +// Add a filter to the camera (v4 feature) +cam.filters.external.addBlur(1, 2); +``` +</quick_start> + +<core_concepts> +Every Scene has a `CameraManager` at `this.cameras` (not `this.camera`), registered as a plugin under the key `'CameraManager'`. Key references: `this.cameras.cameras` (array of Camera objects in render order), `this.cameras.main` (the "main" camera, `cameras[0]` by default), `this.cameras.default` (an un-transformed utility camera, not in the cameras array). + +Key CameraManager methods: `add(x?, y?, width?, height?, makeMain?, name?)` creates a Camera (defaults to full game size at 0,0); `addExisting(camera, makeMain?)` adds a pre-built Camera; `remove(camera, runDestroy?)` removes and optionally destroys (if main is removed, resets to `cameras[0]`); `getCamera(name)` finds by name; `getTotal(isVisible?)` counts cameras; `fromJSON(config)` creates cameras from config; `resetAll()` destroys all and creates one fresh default; `resize(width, height)` resizes all cameras. The manager supports up to 32 cameras that can use `ignore()` for object exclusion (IDs are bitmasks); cameras beyond 32 get ID 0 and cannot exclude objects. + +The `main` property is a convenience reference, set automatically when the scene boots (first camera created becomes main), when you pass `makeMain: true` to `add`/`addExisting`, or when the current main is removed (falls back to `cameras[0]`). + +A Camera has two independent coordinate concepts. The **viewport** is the physical rectangle on the canvas where it renders, controlled by `setPosition(x, y)`, `setSize(w, h)`, or `setViewport(x, y, w, h)` (defaults to filling the canvas). **Scroll** is where the camera is looking in the world, controlled by `scrollX`/`scrollY` or `setScroll(x, y)` — scrolling does not affect the viewport. `worldView` is a read-only `Rectangle` updated each frame reflecting what world area the camera can currently see (accounting for scroll, zoom, and bounds), useful for culling or intersection checks. + +```js +// Viewport: a 320x200 mini-map in the top-right corner +const miniCam = this.cameras.add(480, 0, 320, 200); +// Scroll: make the mini-map look at a different world area +miniCam.setScroll(1000, 500); +// Zoom: the mini-cam shows more of the world +miniCam.setZoom(0.25); +``` +</core_concepts> + +<common_patterns> +Scrolling: +```js +cam.scrollX = 100; cam.scrollY = 200; // direct property access +cam.setScroll(100, 200); // chainable setter +cam.centerOn(500, 400); // center on a world coordinate +const point = cam.getScroll(500, 400); // scroll values to center on a point (Vector2), without moving +``` + +Following a sprite — `startFollow` accepts any object with `x`/`y` (not just Game Objects). Lerp of `1` snaps instantly, `0.1` gives smooth tracking, `0` on an axis disables tracking on that axis. With a deadzone set, the camera does not scroll while the target remains inside the deadzone rectangle (re-centered on the camera midpoint each frame). +```js +cam.startFollow(player); // instant follow (lerp = 1, default) +cam.startFollow(player, false, 0.1, 0.1); // smooth follow +// startFollow(target, roundPixels?, lerpX?, lerpY?, offsetX?, offsetY?) +cam.startFollow(player, true, 0.05, 0.05, 0, -50); + +cam.setLerp(0.08, 0.08); // change lerp after starting +cam.setFollowOffset(0, -50); // change offset after starting +cam.setDeadzone(200, 150); // camera only scrolls when target leaves this rect +cam.stopFollow(); +``` + +Zoom — never set zoom to 0; the minimum is clamped to 0.001: +```js +cam.setZoom(2); // 2x zoom in +cam.setZoom(0.5); // zoom out (see twice as much) +cam.setZoom(2, 1); // independent horizontal/vertical zoom +cam.zoom; // shortcut -- reads zoomX (assumes uniform) +cam.zoomX; cam.zoomY; +``` + +Bounds — bounds only restrict scrolling; they do not prevent placing Game Objects outside, nor affect the viewport position: +```js +cam.setBounds(0, 0, worldWidth, worldHeight); +cam.setBounds(0, 0, 2048, 2048, true); // center on new bounds immediately +cam.useBounds = false; // temporarily disable without removing +cam.removeBounds(); // remove entirely +const rect = cam.getBounds(); // returns a new Rectangle copy +``` + +Multiple cameras — a dual-camera layering pattern (minimaps, in-canvas effect layers; screen-space HUD belongs in the React overlay): one camera ignores world objects and the other ignores layer objects: +```js +const main = this.cameras.main; + +const minimap = this.cameras.add(600, 0, 200, 150).setZoom(0.2).setName('minimap'); +minimap.setScroll(0, 0); +minimap.setBackgroundColor('rgba(0,0,0,0.5)'); + +const hudCam = this.cameras.add(0, 0, 800, 600).setName('hud'); +hudCam.setScroll(0, 0); + +main.ignore(hudGroup); // main ignores HUD objects +hudCam.ignore(worldGroup); // HUD camera ignores world objects + +const found = this.cameras.getCamera('minimap'); +this.cameras.remove(minimap); +``` + +Camera effects — all are on the `Camera` class (not `BaseCamera`), each returns `this` for chaining, and an already-running effect will not restart unless you pass `force: true`. + +Fade — `fadeOut`/`fade` goes transparent-to-color; `fadeIn`/`fadeFrom` goes color-to-transparent: +```js +cam.fadeOut(1000); // to black over 1s +cam.fadeOut(1000, 255, 0, 0); // to red +cam.fadeIn(500); // from black over 500ms +cam.fade(1000, 0, 0, 0, true); // lower-level, force start even if running +cam.fadeFrom(1000, 0, 0, 0, true); +cam.fadeOut(1000, 0, 0, 0, (cam, progress) => { /* progress 0..1 */ }); +``` + +Flash — `flash(duration?, r?, g?, b?, force?, callback?, context?)`: +```js +cam.flash(250); // white flash, default 250ms +cam.flash(500, 255, 0, 0); // red flash +cam.flash(300, 255, 255, 255, true, (cam, progress) => {}); +``` + +Shake — `shake(duration?, intensity?, force?, callback?, context?)`. The intensity is a small float; the default 0.05 shifts up to 5% of the viewport size: +```js +cam.shake(100); // 100ms at intensity 0.05 +cam.shake(500, 0.02); // stronger, longer +cam.shake(300, new Phaser.Math.Vector2(0.1, 0.02)); // independent x/y intensity +``` + +Pan — scrolls the camera so its viewport center finishes at the given world coordinate; `pan(x, y, duration?, ease?, force?, callback?, context?)`: +```js +cam.pan(400, 300, 2000); +cam.pan(400, 300, 2000, 'Power2'); +cam.pan(800, 600, 1500, 'Sine.easeInOut', false, (cam, progress, x, y) => {}); +``` + +ZoomTo — `zoomTo(zoom, duration?, ease?, force?, callback?, context?)`: +```js +cam.zoomTo(2, 1000); +cam.zoomTo(0.5, 2000, 'Cubic.easeOut'); +``` + +RotateTo — angle is in **radians**; set `shortestPath` to take the shortest direction; `rotateTo(angle, shortestPath?, duration?, ease?, force?, callback?, context?)`: +```js +cam.rotateTo(Phaser.Math.DegToRad(45), false, 1000); +cam.rotateTo(Math.PI, true, 1500, 'Quad.easeInOut'); +``` + +Reset all running effects (rotate, pan, shake, flash, fade): +```js +cam.resetFX(); +``` + +Keyboard controls — both classes require `update(delta)` in your scene's update method and support `zoomIn`/`zoomOut` keys plus a `zoomSpeed` config value. `FixedKeyControl` moves at a fixed speed per frame (no smoothing); `SmoothedKeyControl` applies acceleration, drag, and max speed. +```js +// FixedKeyControl +const cursors = this.input.keyboard.createCursorKeys(); +this.camControl = new Phaser.Cameras.Controls.FixedKeyControl({ + camera: this.cameras.main, + left: cursors.left, right: cursors.right, up: cursors.up, down: cursors.down, + speed: 0.5 // can also be { x: 0.5, y: 0.3 } +}); +// In update(time, delta): this.camControl.update(delta); + +// SmoothedKeyControl +this.camControl = new Phaser.Cameras.Controls.SmoothedKeyControl({ + camera: this.cameras.main, + left: cursors.left, right: cursors.right, up: cursors.up, down: cursors.down, + zoomIn: this.input.keyboard.addKey('Q'), + zoomOut: this.input.keyboard.addKey('E'), + zoomSpeed: 0.02, acceleration: 0.06, drag: 0.0005, maxSpeed: 1.0 +}); +// In update(time, delta): this.camControl.update(delta); +``` + +Ignore lists — `ignore` updates a Game Object's `cameraFilter` bitmask so the camera skips rendering it. This is the primary mechanism for layered-camera setups such as minimaps. +```js +cam.ignore(uiText); // single object +cam.ignore([scoreText, livesIcon, pauseButton]); // array +cam.ignore(uiGroup); // all children in a Group +cam.ignore(uiLayer); // a Layer and all its children +``` + +Camera deadzone — a rectangular area around the follow target where the camera does not scroll: +```js +cam.setDeadzone(200, 150); // 200x150 centered on the viewport +console.log(cam.deadzone); // Phaser.Geom.Rectangle or null (read-only) +cam.setDeadzone(); // remove +``` + +World point conversion — essential with scrolled/zoomed cameras, since raw pointer coordinates are screen space: +```js +const worldPoint = cam.getWorldPoint(pointer.x, pointer.y); +const output = new Phaser.Math.Vector2(); // reuse to avoid allocation +cam.getWorldPoint(pointer.x, pointer.y, output); +``` + +Render list — `cam.renderList` is an array of Game Objects visible to this camera, rebuilt every frame. + +Force composite — render the camera to an offscreen framebuffer (required for `CaptureFrame` and similar). Filters enable this automatically; use it explicitly when you need it without filters: +```js +cam.setForceComposite(true); +cam.setForceComposite(false); +``` + +Filters on cameras (v4) — `Camera` has a `filters` property with two `FilterList` instances: `cam.filters.internal` (system-applied) and `cam.filters.external` (user-added). Add post-processing the same way as on Game Objects. Filters require WebGL and force framebuffer rendering when active. +```js +cam.filters.external.addBlur(1, 2); +cam.filters.external.addGlow(0xff0000, 4, 0, false, 0.1, 10); +const fx = cam.filters.external.addColorMatrix(); +fx.grayscale(); +cam.filters.external.clear(); // remove all external filters +``` +</common_patterns> + +<events> +Camera events are emitted on the Camera instance itself (it extends `EventEmitter`); listen with `cam.on(event, handler)`. Constants live under `Phaser.Cameras.Scene2D.Events`: + +- `FADE_IN_START` / `FADE_IN_COMPLETE` — `fadeIn`/`fadeFrom` begins / fade-in finishes +- `FADE_OUT_START` / `FADE_OUT_COMPLETE` — `fadeOut`/`fade` begins / fade-out finishes +- `FLASH_START` / `FLASH_COMPLETE` +- `SHAKE_START` / `SHAKE_COMPLETE` +- `PAN_START` / `PAN_COMPLETE` +- `ZOOM_START` / `ZOOM_COMPLETE` +- `ROTATE_START` / `ROTATE_COMPLETE` +- `FOLLOW_UPDATE` — each frame while following; args `(camera, target)` +- `PRE_RENDER` / `POST_RENDER` — before / after the camera renders +- `DESTROY` — camera is destroyed + +```js +cam.on(Phaser.Cameras.Scene2D.Events.FADE_OUT_COMPLETE, () => { + this.scene.start('GameOver'); +}); +``` +</events> +</cameras> diff --git a/skills/base44-platform-games/references/phaser/curves_and_paths.md b/skills/base44-platform-games/references/phaser/curves_and_paths.md new file mode 100644 index 0000000..2ea4ceb --- /dev/null +++ b/skills/base44-platform-games/references/phaser/curves_and_paths.md @@ -0,0 +1,182 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: curves and paths + +<curves_and_paths> +Use this when working with curves and paths in Phaser 4: creating paths from curves (splines, bezier, lines, ellipses), getting points along them, drawing them with Graphics, and making sprites follow paths automatically with PathFollower. + +<quick_start> +```js +// In a Scene's create() method: + +// 1. Create a Path starting at (50, 300) +const path = this.add.path(50, 300); + +// 2. Add curves to the path +path.lineTo(200, 100); +path.splineTo([ new Phaser.Math.Vector2(300, 400), new Phaser.Math.Vector2(500, 200) ]); +path.lineTo(700, 300); + +// 3. Draw the path using Graphics +const graphics = this.add.graphics(); +graphics.lineStyle(2, 0xffffff, 1); +path.draw(graphics, 64); + +// 4. Create a PathFollower sprite that moves along the path +const follower = this.add.follower(path, 50, 300, 'ship'); +follower.startFollow({ + duration: 5000, + rotateToPath: true, + repeat: -1, + yoyo: true +}); +``` +</quick_start> + +<core_concepts> +A `Phaser.Curves.Path` is a container that combines multiple Curves into one continuous compound curve. Curves in a Path do not need to be connected end-to-end; only their order affects point calculations. Created via `this.add.path(x, y)` where x/y is the starting point. Key properties: `curves` (array of `Phaser.Curves.Curve`), `startPoint` (Vector2), `autoClose` (if true, `getPoints()` appends the first point at the end), `defaultDivisions` (default 12, divisions per curve for `getPoints()`), `name`. + +All curve types extend `Phaser.Curves.Curve`. Every curve supports: `getPoint(t, out)` (point at parameter t 0-1), `getPointAt(u, out)` (point at arc-length position u 0-1, evenly spaced), `getPoints(divisions, stepRate, out)`, `getSpacedPoints(divisions, stepRate, out)` (equidistant by arc length), `getDistancePoints(distance)`, `getLength()`, `getBounds(out, accuracy)`, `getTangent(t, out)` / `getTangentAt(u, out)` (unit tangent vector), `getStartPoint(out)` / `getEndPoint(out)`, `getRandomPoint(out)`, `draw(graphics, pointsTotal)`, and `active` (when false, the parent Path skips this curve). + +A `Phaser.GameObjects.PathFollower` is a Sprite with the `Components.PathFollower` mixin. It uses an internal Tween (a number counter from 0 to 1) to advance along a Path each frame. Created via `this.add.follower(path, x, y, texture, frame)`. The component provides: `path` (the Path being followed), `pathTween` (internal Tween driving movement), `pathOffset` (Vector2 offset added to path coordinates), `pathVector` (current position on path), `pathDelta` (distance traveled since last frame), `rotateToPath` (auto-rotate to face path direction), `pathRotationOffset` (degrees added to auto-rotation). +</core_concepts> + +<common_patterns> +Path has convenience methods that create curves starting from the previous end point: +```js +const path = this.add.path(100, 500); + +path.lineTo(300, 100); // straight line +path.cubicBezierTo(500, 100, 350, 50, 450, 50); // cubic bezier (endX, endY, cp1X, cp1Y, cp2X, cp2Y) +path.quadraticBezierTo(700, 400, 600, 100); // quadratic bezier (endX, endY, cpX, cpY) +path.splineTo([ // spline through points + new Phaser.Math.Vector2(750, 300), + new Phaser.Math.Vector2(600, 500) +]); +path.ellipseTo(50, 80, 0, 270, false, 0); // ellipse arc (xRadius, yRadius, startAngle, endAngle, clockwise, rotation) +path.circleTo(40); // shortcut for ellipseTo with equal radii and 0-360 + +// Jump to a new position without drawing (creates a gap) +path.moveTo(400, 400); +path.lineTo(500, 400); + +// Close the path by connecting end to start +path.closePath(); +``` + +Adding pre-constructed standalone curve objects: +```js +const path = new Phaser.Curves.Path(0, 0); + +const line = new Phaser.Curves.Line(new Phaser.Math.Vector2(0, 0), new Phaser.Math.Vector2(200, 200)); +path.add(line); + +const spline = new Phaser.Curves.Spline([ 200, 200, 300, 100, 400, 300 ]); +path.add(spline); + +const ellipse = new Phaser.Curves.Ellipse(400, 300, 100, 60, 0, 360, false, 0); +path.add(ellipse); +``` + +Getting points along a path: +```js +const points = path.getPoints(); // array of points (uses defaultDivisions per curve) +const detailed = path.getPoints(32); // explicit divisions per curve +const spaced = path.getSpacedPoints(100); // equally spaced points along entire path +const midpoint = path.getPoint(0.5); // single point at normalized position (0-1) +const tangent = path.getTangent(0.5); // tangent vector at a position +const length = path.getLength(); // total path length in pixels +const bounds = path.getBounds(); // bounding rectangle +``` + +Drawing paths with Graphics: +```js +const graphics = this.add.graphics(); + +// Draw entire path +graphics.lineStyle(2, 0x00ff00, 1); +path.draw(graphics, 64); // 64 = points per curve for smoothness + +// Draw individual curves +graphics.lineStyle(1, 0xff0000, 1); +path.curves[0].draw(graphics, 32); + +// Draw debug points +const points = path.getSpacedPoints(50); +points.forEach(p => { + graphics.fillStyle(0xffff00, 1); + graphics.fillCircle(p.x, p.y, 3); +}); +``` + +PathFollower sprite with full config: +```js +const path = this.add.path(100, 200); +path.lineTo(400, 400); +path.lineTo(700, 200); + +const enemy = this.add.follower(path, 100, 200, 'enemy'); + +enemy.startFollow({ + duration: 3000, // ms to traverse path + positionOnPath: true, // snap to path start position + rotateToPath: true, // auto-rotate to face direction + rotationOffset: 90, // offset added to auto-rotation (degrees) + repeat: -1, // -1 = infinite repeat + yoyo: true, // reverse on each repeat + from: 0, // start position on path (0-1) + to: 1, // end position on path (0-1) + startAt: 0, // initial seek position + ease: 'Sine.easeInOut' // any valid Phaser ease +}); + +// Control during playback +enemy.pauseFollow(); +enemy.resumeFollow(); +enemy.stopFollow(); +enemy.isFollowing(); // returns boolean + +// Change path at runtime +enemy.setPath(newPath); +enemy.setPath(newPath, { duration: 2000 }); // auto-starts + +// Set rotation independently +enemy.setRotateToPath(true, 90); // (value, offsetDegrees) +``` + +The follow config also accepts a bare duration number as shorthand — `enemy.startFollow(5000)` is equivalent to `enemy.startFollow({ duration: 5000 })`. The config additionally accepts all standard Tween properties (`ease`, `repeat`, `yoyo`, `delay`, `hold`, `onComplete`, etc.); defaults are `duration: 1000`, `from: 0`, `to: 1`, `positionOnPath: false`, `rotateToPath: false`, `rotationOffset: 0`, `startAt: 0`. +</common_patterns> + +<curve_types> +- **Line** (`Phaser.Curves.Line`): straight segment between two points. Params `(p0, p1)` as Vector2 or `([x0,y0,x1,y1])`. +- **Spline** (`Phaser.Curves.Spline`): Catmull-Rom spline through control points. Params `(points)` as array of Vector2, flat numbers, or nested arrays. +- **CubicBezier** (`Phaser.Curves.CubicBezier`): start, 2 control points, end. Params `(p0, p1, p2, p3)` or `([x0,y0,...x3,y3])`. +- **QuadraticBezier** (`Phaser.Curves.QuadraticBezier`): start, 1 control point, end. Params `(p0, p1, p2)` or `([x0,y0,...x2,y2])`. +- **Ellipse** (`Phaser.Curves.Ellipse`): elliptical arc, angles in degrees, yRadius defaults to xRadius. Params `(x, y, xRadius, yRadius, startAngle, endAngle, clockwise, rotation)` or a config object. + +The Ellipse curve has get/set properties for runtime modification: `x`, `y` (center), `xRadius`, `yRadius`, `startAngle`/`endAngle` (degrees — internally converted to/from radians), `clockwise`, `rotation` (radians), `angle` (rotation in degrees, alternative to `rotation`), and `setWidth(value)` / `setHeight(value)` (set radius to value/2). +</curve_types> + +<gotchas> +1. **`getPoint(t)` vs `getPointAt(u)` on curves.** `getPoint` uses the raw curve parameter t, which does not produce evenly spaced points on most curve types. `getPointAt` maps through arc length for even spacing. On a Path, `getPoint` already accounts for arc length across the whole path. + +2. **Path `moveTo` creates an inactive curve.** The `MoveTo` pseudo-curve has `active: false` and zero length. It only repositions the end point for the next curve — it does not draw anything and is skipped by `getPoints()` and `draw()`. + +3. **PathFollower uses a Tween internally.** The `startFollow` config is passed to `scene.tweens.addCounter()`. All tween properties (ease, delay, repeat, yoyo, callbacks) work. The tween is set to `persist: true` automatically. + +4. **PathFollower offset behavior.** When `positionOnPath: false` (default), the follower's current position becomes the offset from the path start. When `positionOnPath: true`, the follower snaps to the path's start point and the offset is zeroed. + +5. **Ellipse angles are in degrees.** The constructor and `startAngle`/`endAngle` accept degrees (stored internally as radians). The `rotation` property is in radians, but `angle` is in degrees. + +6. **`closePath` vs `autoClose`.** `closePath()` adds an explicit Line curve from end to start. `autoClose = true` only affects `getPoints()` and `getSpacedPoints()` output by appending the first point, without adding a curve. + +7. **Cached lengths can go stale.** `getCurveLengths()` caches results based on array length only. If you modify a curve's control points, call `path.updateArcLengths()` to force recalculation. + +8. **`cubicBezierTo` parameter order with numbers.** When passing numbers: `cubicBezierTo(endX, endY, cp1X, cp1Y, cp2X, cp2Y)` — the end point comes first, not the control points. When passing Vector2 objects: `cubicBezierTo(cp1, cp2, endPoint)`. + +9. **Spline needs at least 4 points.** The Catmull-Rom interpolation works best with 4+ points. With fewer, the curve may not behave as expected. + +10. **Line curve `arcLengthDivisions` is 1.** Unlike other curves (default 100), Line overrides this to 1 since a line is inherently uniform. No need to adjust it. +</gotchas> +</curves_and_paths> diff --git a/skills/base44-platform-games/references/phaser/data_manager.md b/skills/base44-platform-games/references/phaser/data_manager.md new file mode 100644 index 0000000..29a37e9 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/data_manager.md @@ -0,0 +1,236 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: data manager + +<data_manager> +Use this when storing runtime key-value data on game objects, scenes, or globally in Phaser 4, listening for data change events, or managing reactive game state. The DataManager provides runtime key-value storage with event-driven change tracking at three levels: per-GameObject (`sprite.setData`/`getData`), per-Scene (`this.data`), and global (`this.registry`). + +<quick_start> +```js +// Per-GameObject data (auto-creates DataManager on first use) +const gem = this.add.sprite(100, 100, 'gem'); +gem.setData('value', 50); +gem.setData({ color: 'red', level: 2 }); +gem.getData('value'); // 50 +gem.getData(['value', 'color']); // [50, 'red'] + +// Increment / toggle helpers +gem.incData('value', 10); // value is now 60 +gem.incData('value', -5); // value is now 55 (negative to decrement) +gem.toggleData('active'); // false -> true (starts from false if unset) + +// Scene-level data (this.data is a DataManagerPlugin) +this.data.set('score', 0); +this.data.get('score'); // 0 +this.data.values.score += 100; // triggers changedata event + +// Global registry (shared across ALL scenes) +this.registry.set('highScore', 9999); +this.registry.get('highScore'); // 9999, readable from any scene +``` +</quick_start> + +<core_concepts> +`Phaser.Data.DataManager` is the base class storing key-value pairs in an internal `list` object. Core methods: `set(key, value)` (stores a value; emits `setdata` for a new key or `changedata` + `changedata-{key}` for an existing one; accepts an object to set multiple keys), `get(key)` (retrieves a value, or pass an array of keys for an array of values), `inc(key, amount)` (increments numeric value, default +1, creates from 0 if missing), `toggle(key)` (flips a boolean, creates from `false` if missing), `remove(key)` (deletes a key, emits `removedata`, accepts an array), `has(key)`, `getAll()` (shallow copy of all pairs as a plain object), `query(regex)` (entries whose keys match a RegExp), `each(callback, context, ...args)` (iterates; callback `(parent, key, value, ...args)`), `merge(data, overwrite)` (bulk import; `overwrite` defaults `true`), `pop(key)` (get and delete in one call), `reset()` (clears all data and unfreezes), `freeze` / `setFreeze(bool)` (when frozen, all writes silently no-op), and `count` (read-only entry count). + +The `values` proxy object allows direct property access with event emission: +```js +// After set('gold', 100), you can do: +data.values.gold += 50; // emits changedata and changedata-gold +// But you MUST use set() to create a key first — direct assignment +// to values for a new key will NOT set up the event proxy. +``` + +`Phaser.Data.DataManagerPlugin` extends DataManager and is registered as the `data` scene plugin (`this.data` in any Scene). It uses the Scene's event emitter (`scene.sys.events`), so data events fire on the Scene's event bus: +```js +this.data.set('lives', 3); +this.events.on('changedata-lives', (scene, value, previousValue) => { + console.log('Lives changed from', previousValue, 'to', value); +}); +``` +The plugin auto-cleans on scene shutdown (removes its shutdown listener) and fully destroys on scene destroy. + +The registry is a plain `DataManager` instance on the `Game` object (`game.registry`), with its own dedicated `EventEmitter` (not shared with any scene). Every scene gets a reference as `this.registry`. It persists for the lifetime of the Game and is never automatically cleared on scene restart or shutdown: +```js +this.registry.set('currentLevel', 1); // Scene A sets +const level = this.registry.get('currentLevel'); // Scene B reads +// Listen for registry changes (events fire on registry.events, NOT this.events) +this.registry.events.on('changedata-currentLevel', (game, value, previousValue) => {}); +``` + +GameObjects do NOT have a DataManager by default — it is created lazily on first call to `setData()`, `getData()`, `incData()`, or `toggleData()` (or explicitly via `setDataEnabled()`). The DataManager's event emitter is the GameObject itself, so data events fire directly on the GameObject: +```js +const player = this.add.sprite(0, 0, 'player'); +player.setData('hp', 100); +player.on('changedata-hp', (gameObject, value, previousValue) => { + if (value <= 0) { gameObject.destroy(); } +}); +``` +</core_concepts> + +<common_patterns> +Setting and getting: +```js +sprite.setData('speed', 200); +sprite.getData('speed'); // 200 +sprite.setData({ speed: 200, direction: 'left', hp: 100 }); // object form +const [speed, hp] = sprite.getData(['speed', 'hp']); // batch get +sprite.data.values.speed = 300; // emits changedata event (after initial set) +``` + +Listening for changes — prefer key-specific listeners for performance: +```js +sprite.on('changedata', (gameObject, key, value, previousValue) => {}); // ANY change +sprite.on('changedata-hp', (gameObject, value, previousValue) => { // SPECIFIC key (preferred) + this.hpBar.setValue(value); +}); +sprite.on('setdata', (gameObject, key, value) => {}); // new key set (first time) +sprite.on('removedata', (gameObject, key, value) => {}); // key removed +``` + +Global registry for cross-scene state: +```js +// HUD scene watches for score changes set by the Game scene +this.registry.events.on('changedata-score', (game, value) => { + this.scoreText.setText('Score: ' + value); +}); +this.events.once('shutdown', () => { + this.registry.events.off('changedata-score'); +}); +// In GameScene.update(): +this.registry.set('score', this.score); +``` + +Complex data and the object-mutation trap: +```js +sprite.setData('inventory', ['sword', 'shield']); +sprite.setData('stats', { str: 10, dex: 8, int: 12 }); + +// CAUTION: mutating a stored object/array does NOT trigger changedata +const inv = sprite.getData('inventory'); +inv.push('potion'); // No event fired! The reference didn't change. +sprite.setData('inventory', [...inv]); // new array reference triggers changedata +``` + +Merging, querying, and freezing: +```js +sprite.data.merge({ hp: 100, speed: 200, armor: 0 }, false); // only sets keys that don't exist +sprite.data.merge(savedState, true); // overwrite existing + +this.data.set('enemy_1_hp', 50); +this.data.set('enemy_2_hp', 80); +this.data.set('player_hp', 100); +const enemyData = this.data.query(/^enemy_/); // { enemy_1_hp: 50, enemy_2_hp: 80 } + +sprite.data.freeze = true; // set, remove, inc, toggle all become no-ops +sprite.setData('hp', 0); // silently ignored +sprite.data.freeze = false; +sprite.data.setFreeze(true); // chainable equivalent +``` +</common_patterns> + +<persistence> +The DataManager, `this.data`, and `this.registry` are runtime state only. They are not persistence and must not be used as storage that survives reloads. + +Only persistence path: define a Base44 entity schema (for example `base44/entities/GameScore.jsonc`) and read/write it with the SDK (`base44.entities.*`) from React (`GamePage.jsx` or the game parent). A Phaser scene must never import `@/api/base44Client` or call the SDK. + +Do not persist game data in `localStorage`, auth profile fields, `this.registry`, `this.data`, module globals, config files, or asset URLs. + +Flow: +1. Phaser emits an `EventBus` event with final/progress data. +2. React handles the event and calls `base44.entities.<Entity>.create/filter/update/delete`. +3. React passes loaded entity data back into Phaser through `EventBus` as runtime state. + +```js +// Phaser scene: emit data, do not import or call the SDK here. +EventBus.emit('score-final', { score: this.score, level: this.level }); + +const onScoresLoaded = (scores) => this.registry.set('leaderboard', scores); +EventBus.on('scores-loaded', onScoresLoaded); +this.events.once('shutdown', () => EventBus.off('scores-loaded', onScoresLoaded)); +``` + +```jsx +// React page/parent: the only persistence layer. +import { base44 } from '@/api/base44Client'; +import { EventBus } from '@/game/EventBus'; + +useEffect(() => { + const saveScore = ({ score, level }) => { + void base44.entities.GameScore.create({ score, level }); + }; + const loadScores = async () => { + const scores = await base44.entities.GameScore.filter({}, '-score', 10); + EventBus.emit('scores-loaded', scores); + }; + + EventBus.on('score-final', saveScore); + void loadScores(); + return () => EventBus.off('score-final', saveScore); +}, []); +``` + +Define the entity schema in the same response as the code that uses it so `base44.entities.<Entity>` exists. Leaderboards use open `read` and owner-scoped writes; never pass owner fields manually. +</persistence> + +<events> +All data events are defined in `Phaser.Data.Events`. The emitter depends on context: for GameObjects it is the GameObject itself; for scene data it is `scene.sys.events` (so `this.events`); for the registry it is `registry.events`. + +- `SET_DATA` (`'setdata'`) — a new key is created. Args `(parent, key, value)`. +- `CHANGE_DATA` (`'changedata'`) — an existing key's value changes. Args `(parent, key, value, previousValue)`. +- `CHANGE_DATA_KEY` (`'changedata-'`) — a specific key changes; append the key name. Args `(parent, value, previousValue)` — note `key` is omitted. +- `REMOVE_DATA` (`'removedata'`) — a key is removed. Args `(parent, key, value)`. +- `DESTROY` (`'destroy'`) — the DataManager's parent is destroyed. No args. + +The `parent` argument is the owner of the DataManager (the GameObject, Scene, or Game instance). `CHANGE_DATA_KEY` is a prefix — the actual event string is `'changedata-'` + the key name (e.g. setting `score` emits `'changedata-score'`). The key-specific `changedata-{key}` fires only for that exact key; the generic `changedata` fires for every key change. +</events> + +<copies_vs_live_references> +Understanding `get()` vs `values` is critical for correct updates: +```js +// get() returns a copy for primitives — modifying the local variable does NOT update the DataManager +let score = this.data.get('score'); +score += 10; // local copy only, DataManager still has the old value + +// values provides a live proxy — assignment triggers CHANGE_DATA event +this.data.values.score += 10; // updates the DataManager AND emits changedata-score + +// For objects, get() returns the stored reference (not a deep copy) +const stats = this.data.get('stats'); +stats.hp -= 10; // mutates the stored object but does NOT trigger events +this.data.set('stats', { ...stats }); // re-set with new reference to trigger event +``` +</copies_vs_live_references> + +<gotchas> +- **Keys are case-sensitive.** `'gold'` and `'Gold'` are two different keys. Be consistent. + +- **`values` proxy requires `set()` first.** You must call `set(key, value)` to create a key before modifying it via `data.values.key`. Direct assignment to `values` for a brand-new key creates a plain property that emits no events. + +- **Object/array mutation does not trigger events.** Mutating a stored object/array in place does not emit `changedata` because the reference has not changed. Re-set the key with a new reference to trigger the event. + +- **Registry listeners persist across scene restarts.** The registry lives on the Game object, so listeners on `this.registry.events` are NOT cleaned up on scene restart. Always remove them on `SHUTDOWN`: +```js +create() { + const handler = (game, value) => { this.updateHUD(value); }; + this.registry.events.on('changedata-score', handler); + this.events.once('shutdown', () => { + this.registry.events.off('changedata-score', handler); + }); +} +``` + +- **Frozen DataManagers fail silently.** When `freeze` is `true`, all writes (`set`, `remove`, `inc`, `toggle`, `pop`, `merge`) silently do nothing — no error, no event. `setFreeze(true)` takes effect immediately, so freezing mid-batch silently no-ops subsequent writes; freeze only after all writes are complete. + +- **Scene Data Plugin shutdown vs destroy.** The plugin removes its shutdown listener on shutdown but does NOT clear data on shutdown — data persists across restart. It only fully resets on scene destroy. For a clean slate on restart, manually `this.data.reset()` in a shutdown listener. + +- **`inc()` on non-numeric values.** `inc()` uses the `+` operator: on strings it concatenates, on booleans they coerce to 0/1. Only use it on keys you know hold numbers. + +- **`reset()` emits no events.** It clears all data silently — no `removedata` fires for the deleted keys. If you need removal events, iterate and call `remove()` per key. + +- **`getAll()` returns a snapshot.** It returns a new plain object each time, a shallow copy — primitives are independent but object/array values still share references with internal storage. + +- **First argument differs by event.** The generic `changedata` callback receives `(parent, key, value, previousValue)`; the key-specific `changedata-{key}` callback omits `key`: `(parent, value, previousValue)`. +</gotchas> +</data_manager> diff --git a/skills/base44-platform-games/references/phaser/events_system.md b/skills/base44-platform-games/references/phaser/events_system.md new file mode 100644 index 0000000..fb70dbd --- /dev/null +++ b/skills/base44-platform-games/references/phaser/events_system.md @@ -0,0 +1,193 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: events system + +<events_system> +Use this when working with the Phaser 4 event system: EventEmitter, scene events, game events, custom events, and event-driven communication between systems. Phaser uses the EventEmitter pattern (via eventemitter3) throughout — every major system (Game, Scene, Input, Loader, Cameras, Sound, Tweens, Physics, Textures, Animations) is or contains an EventEmitter. Events use lowercase string keys, and Phaser provides named constants for all built-in events to avoid typos. + +<quick_start> +```js +// on — listen for an event (persists until removed) +this.input.on('pointerdown', (pointer) => { + console.log('clicked at', pointer.x, pointer.y); +}); + +// once — listen for an event, auto-removes after first fire +this.events.once('shutdown', () => { + console.log('scene shutting down'); +}); + +// off — remove a specific listener (must pass same function reference) +const handler = (pointer) => { /* ... */ }; +this.input.on('pointerdown', handler); +this.input.off('pointerdown', handler); + +// emit — fire a custom event with arguments +this.events.emit('player-died', this.player, this.score); + +// removeAllListeners — remove all listeners for an event (or all events) +this.events.removeAllListeners('player-died'); +this.events.removeAllListeners(); // all events +``` + +Always prefer named constants over raw strings to prevent typos: +```js +this.events.on(Phaser.Scenes.Events.UPDATE, (time, delta) => { /* runs every frame */ }); +this.input.on(Phaser.Input.Events.POINTER_DOWN, (pointer) => { /* pointer pressed */ }); +this.game.events.on(Phaser.Core.Events.BLUR, () => { /* browser tab lost focus */ }); +``` +</quick_start> + +<core_concepts> +`Phaser.Events.EventEmitter` extends eventemitter3 and adds `shutdown()` and `destroy()` (both call `removeAllListeners()`). Full inherited API: `on(event, fn, context?)` (persistent listener, returns `this`), `addListener` (alias for `on`), `once(event, fn, context?)` (one-time, auto-removed after first fire), `off(event, fn?, context?, once?)` (remove listener — must pass the same `fn` reference), `removeListener` (alias for `off`), `removeAllListeners(event?)` (remove all for one event, or all events), `emit(event, ...args)` (returns `true` if it had listeners), `listeners(event)`, `listenerCount(event)`, `eventNames()`. + +Every built-in event is a lowercase string exported as a constant; the constant name maps predictably to the string: +```js +Phaser.Scenes.Events.UPDATE // 'update' +Phaser.Scenes.Events.PRE_UPDATE // 'preupdate' +Phaser.Scenes.Events.SHUTDOWN // 'shutdown' +Phaser.Core.Events.BOOT // 'boot' +Phaser.Input.Events.POINTER_DOWN // 'pointerdown' +``` + +Some events use a key-suffix pattern for per-key listening — append the key/type to the prefix constant: +```js +// Loader: listen for a specific file completing — string value: 'filecomplete-image-logo' +this.load.on(Phaser.Loader.Events.FILE_KEY_COMPLETE + 'image-logo', (key, type, data) => {}); +// Animations: listen for a specific animation completing — string value: 'animationcomplete-walk' +sprite.on(Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + 'walk', () => {}); +// Textures: listen for a specific texture being added — string value: 'addtexture-myTexture' +this.textures.on(Phaser.Textures.Events.ADD_KEY + 'myTexture', () => {}); +``` + +The third argument to `on`/`once` sets `this` inside the callback (defaults to the emitter). Arrow functions ignore this argument since they capture lexical `this`: +```js +this.input.on('pointerdown', function (pointer) { + this.cameras.main.shake(100); // 'this' = scene (passed as 3rd arg) +}, this); +this.input.on('pointerdown', (pointer) => { + this.cameras.main.shake(100); // 'this' = enclosing scope (scene in create) +}); +``` +</core_concepts> + +<common_patterns> +Scene lifecycle. Frame loop order: `preupdate` -> `update` -> `Scene.update()` -> `postupdate` -> `prerender` -> `render`. Always clean up on shutdown to prevent leaks on scene restart: +```js +create() { + this.events.on(Phaser.Scenes.Events.UPDATE, this.onUpdate, this); + this.events.on(Phaser.Scenes.Events.SHUTDOWN, () => { + this.events.off(Phaser.Scenes.Events.UPDATE, this.onUpdate, this); + this.input.off('pointerdown', this.onPointerDown, this); + }); +} +``` + +Game-level events fire on the Game instance, shared across all scenes (accessed via `this.game.events`): +```js +this.game.events.on(Phaser.Core.Events.BLUR, this.handleBlur, this); +this.game.events.on(Phaser.Core.Events.VISIBLE, this.handleVisible, this); +``` + +Inter-scene communication has three idioms: +```js +// METHOD 1: game.events — a global event bus accessible from all scenes +this.game.events.emit('score-changed', this.score); // Scene A +this.game.events.on('score-changed', (score) => { this.scoreText.setText(score); }); // Scene B + +// METHOD 2: this.registry — a shared DataManager across all scenes +this.registry.set('score', 100); // Scene A +this.registry.events.on('changedata-score', (parent, value, previousValue) => { // Scene B + this.scoreText.setText(value); +}); + +// METHOD 3: Direct scene access via ScenePlugin +this.scene.get('UIScene').events.emit('update-health', hp); +``` + +Custom events carry arbitrary data arguments: +```js +this.events.emit('player-died', this.player, { lives: this.lives }); +this.events.on('player-died', (player, data) => { + console.log('Lives remaining:', data.lives); +}); +``` + +A standalone EventEmitter can be created for your own buses: +```js +const bus = new Phaser.Events.EventEmitter(); +bus.on('inventory-changed', (items) => { console.log(items.length); }); +bus.emit('inventory-changed', this.inventory); +``` +</common_patterns> + +<event_namespaces> +**Scene Events** (`Phaser.Scenes.Events`) — emitter `this.events`: `BOOT` `'boot'`, `READY` `'ready'`, `START` `'start'`, `CREATE` `'create'`, `PRE_UPDATE` `'preupdate'`, `UPDATE` `'update'`, `POST_UPDATE` `'postupdate'`, `PRE_RENDER` `'prerender'`, `RENDER` `'render'`, `PAUSE` `'pause'`, `RESUME` `'resume'`, `SLEEP` `'sleep'`, `WAKE` `'wake'`, `SHUTDOWN` `'shutdown'` (stopping, may restart), `DESTROY` `'destroy'` (permanent), `ADDED_TO_SCENE` `'addedtoscene'`, `REMOVED_FROM_SCENE` `'removedfromscene'`, plus transition events (`TRANSITION_INIT`/`START`/`OUT`/`COMPLETE`/`WAKE`). + +**Game Events** (`Phaser.Core.Events`) — emitter `this.game.events`: `BOOT` `'boot'`, `READY` `'ready'`, `SYSTEM_READY` `'systemready'`, `PRE_STEP`/`STEP`/`POST_STEP`, `PRE_RENDER`/`POST_RENDER`, `PAUSE`/`RESUME`, `BLUR` `'blur'` (tab lost focus), `FOCUS` `'focus'`, `HIDDEN`/`VISIBLE` (Page Visibility API), `CONTEXT_LOST` `'contextlost'` (WebGL context lost), `DESTROY` `'destroy'`. + +**Input Events** (`Phaser.Input.Events`) — emitter `this.input` (scene-level) or interactive GameObjects. Scene-level pointer: `POINTER_DOWN`/`UP`/`MOVE`/`OVER`/`OUT`, `POINTER_WHEEL` `'wheel'`, `POINTER_DOWN_OUTSIDE`/`UP_OUTSIDE`. Scene-level gameobject: `GAMEOBJECT_DOWN`/`UP`/`MOVE`/`OVER`/`OUT`/`WHEEL`. Per-GameObject (emitted on the GameObject, requires `setInteractive()`): `GAMEOBJECT_POINTER_DOWN` `'pointerdown'` etc. Drag (on `this.input` and on GameObjects): `DRAG_START`/`DRAG`/`DRAG_END`/`DRAG_ENTER`/`DRAG_OVER`/`DRAG_LEAVE`/`DROP`. Other: `GAME_OUT`, `GAME_OVER`, `POINTERLOCK_CHANGE`. + +**Loader Events** (`Phaser.Loader.Events`) — emitter `this.load`: `ADD` `'addfile'`, `START`, `PROGRESS` (0-1), `FILE_LOAD` `'load'`, `FILE_PROGRESS`, `FILE_COMPLETE`, `FILE_KEY_COMPLETE` `'filecomplete-'` (append `type-key`), `FILE_LOAD_ERROR` `'loaderror'`, `POST_PROCESS`, `COMPLETE`. + +**Animation Events** (`Phaser.Animations.Events`) — emitter individual sprites or `this.anims`: `ADD_ANIMATION`, `REMOVE_ANIMATION`, `PAUSE_ALL`/`RESUME_ALL`, `ANIMATION_START`/`RESTART`/`REPEAT`/`UPDATE`/`COMPLETE`/`STOP`, `ANIMATION_COMPLETE_KEY` `'animationcomplete-'` (append key). + +**Camera Events** (`Phaser.Cameras.Scene2D.Events`) — emitter individual camera (e.g. `this.cameras.main`); each effect has a START/COMPLETE pair: `DESTROY`, `FADE_IN_START`/`COMPLETE`, `FADE_OUT_START`/`COMPLETE`, `FLASH_START`/`COMPLETE`, `PAN_START`/`COMPLETE`, `ROTATE_START`/`COMPLETE`, `SHAKE_START`/`COMPLETE`, `ZOOM_START`/`COMPLETE`, `FOLLOW_UPDATE`, `PRE_RENDER`/`POST_RENDER`. + +**Sound Events** (`Phaser.Sound.Events`) — emitter individual sounds or `this.sound`. Per-sound: `PLAY`/`PAUSE`/`RESUME`/`STOP`/`COMPLETE`/`LOOP`/`LOOPED`/`SEEK`/`MUTE`/`VOLUME`/`RATE`/`DETUNE`/`PAN`/`DECODED`/`DESTROY`. SoundManager-level (on `this.sound`): `GLOBAL_MUTE`/`GLOBAL_VOLUME`/`GLOBAL_RATE`/`GLOBAL_DETUNE`, `PAUSE_ALL`/`RESUME_ALL`/`STOP_ALL`, `DECODED_ALL`, `UNLOCKED`. + +**Tween Events** (`Phaser.Tweens.Events`) — emitter individual tweens: `TWEEN_ACTIVE`/`START`/`UPDATE`/`YOYO`/`REPEAT`/`LOOP`/`PAUSE`/`RESUME`/`COMPLETE`/`STOP`. + +**Arcade Physics Events** (`Phaser.Physics.Arcade.Events`) — emitter `this.physics.world`: `COLLIDE`, `OVERLAP`, `TILE_COLLIDE`, `TILE_OVERLAP`, `WORLD_BOUNDS`, `WORLD_STEP`, `PAUSE`, `RESUME`. + +**Texture Events** (`Phaser.Textures.Events`) — emitter `this.textures`: `ADD` `'addtexture'`, `ADD_KEY` `'addtexture-'` (append key), `REMOVE`/`REMOVE_KEY`, `LOAD` `'onload'`, `ERROR` `'onerror'`, `READY`. + +**GameObject Events** (`Phaser.GameObjects.Events`) — emitter individual GameObjects: `ADDED_TO_SCENE`, `REMOVED_FROM_SCENE`, `DESTROY`. Video GameObjects also emit `VIDEO_PLAY`/`PLAYING`/`COMPLETE`/`LOOP`/`STOP`/`CREATED`/`ERROR`/`LOCKED`/`UNLOCKED`/`METADATA`/`SEEKED`/`SEEKING`/`STALLED`/`TEXTURE`/`UNSUPPORTED`. + +**Time Events** (`Phaser.Time.Events`) — emitter `Phaser.Time.TimerEvent` instances: `COMPLETE` `'complete'` (TimerEvent finishes all repeats). +</event_namespaces> + +<gotchas> +- **Memory leaks from unremoved listeners** — the most common bug. `on()` does not auto-remove, so if a scene restarts via `scene.restart()`, old listeners persist and each restart adds duplicates. Always clean up in a `SHUTDOWN` handler, or use `once()` for events you only need fired once: +```js +// BAD: leaks listeners on every scene restart +create() { this.input.on('pointerdown', this.shoot, this); } + +// GOOD: clean up in shutdown +create() { + this.input.on('pointerdown', this.shoot, this); + this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { + this.input.off('pointerdown', this.shoot, this); + }); +} +``` + +- **shutdown vs destroy.** `SHUTDOWN` fires when a scene stops but can restart later — clean up listeners here. `DESTROY` fires when a scene is permanently removed — use for final cleanup. A scene restart fires `SHUTDOWN` then `START` then `CREATE`; it does NOT fire `DESTROY`. + +- **Context binding.** The third argument to `on`/`once` sets `this` inside the callback. Without it, `this` defaults to the emitter, not the scene. Pass `this` with regular functions, or use arrow functions (which capture lexical `this`). + +- **`off()` requires exact references.** It only works if you pass the exact same function reference (and context) used with `on()`. Anonymous or inline arrow functions cannot be removed — store a reference or use a class method: +```js +// BAD: arrow function cannot be removed later +this.events.on('update', () => { this.doStuff(); }); +// GOOD: named method can be removed +this.events.on('update', this.onUpdate, this); +this.events.off('update', this.onUpdate, this); +``` + +- **`once()` auto-removes** the listener after first fire — no manual cleanup needed. + +- **Input event hierarchy.** Input events fire in order: (1) `GAMEOBJECT_POINTER_DOWN` on the GameObject, (2) `GAMEOBJECT_DOWN` on `this.input`, (3) `POINTER_DOWN` on `this.input`. Higher handlers can stop propagation. + +- **Game events vs scene events.** `this.game.events` and `this.events` are different emitters. Game events fire once per game loop tick across all scenes; scene events fire per-scene. Listeners on `game.events` persist across scene restarts — always clean them up on `SHUTDOWN`: +```js +create() { + this.game.events.on(Phaser.Core.Events.BLUR, this.onBlur, this); + this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { + this.game.events.off(Phaser.Core.Events.BLUR, this.onBlur, this); + }); +} +``` +</gotchas> +</events_system> diff --git a/skills/base44-platform-games/references/phaser/filters_and_postfx.md b/skills/base44-platform-games/references/phaser/filters_and_postfx.md new file mode 100644 index 0000000..e0c9eeb --- /dev/null +++ b/skills/base44-platform-games/references/phaser/filters_and_postfx.md @@ -0,0 +1,198 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: filters and postfx + +<filters_and_postfx> +Use this when applying visual filters or post-processing effects in Phaser 4: bloom, blur, glow, color matrix, barrel distortion, displacement, masks, custom shaders, and the filter pipeline. Filters are GPU-based, WebGL-only effects applied after an object or camera renders to a texture. + +<quick_start> +```js +// Add a glow to a sprite +const sprite = this.add.sprite(400, 300, 'player'); +sprite.enableFilters(); // Step 1: enable filters (WebGL only) +sprite.filters.internal.addGlow(0xff00ff, 4, 0, 1); // Step 2: add via .filters.internal or .filters.external + +// Add a blur to the camera (cameras have filters enabled by default) +const camera = this.cameras.main; +camera.filters.internal.addBlur(0, 2, 2, 1); +``` +</quick_start> + +<core_concepts> +Each filter runs a shader pass over the rendered texture, producing the final visual output. The rendering pipeline for a camera with filters: (1) objects render to a texture the size of the camera; (2) **internal filters** process that texture in object/camera local space; (3) the texture is drawn to a context-sized texture, applying camera transforms (position, rotation, zoom); (4) **external filters** process that context texture in screen space; (5) the final texture is composited into the output. + +Every `FilterList` exposes two sub-lists, `filters.internal` and `filters.external`, controlling **when** the filter runs relative to the camera/object transform: +- **Internal** — applied before the camera transform, operating in the object's local coordinate space. A horizontal blur on a rotated object appears rotated with the object. Internal filters only cover the object/camera region, so they are cheaper. Use internal wherever possible. +- **External** — applied after the camera transform, operating in screen space. A horizontal blur on a rotated object always blurs horizontally on screen. External filters are full-screen and more expensive. + +`FilterList` (`Phaser.GameObjects.Components.FilterList`) is the container holding filter controllers. It provides `add(filter, index)` (add a Controller at an optional index), `remove(filter, forceDestroy)`, `clear()` (remove and destroy all), `getActive()` (all filters where `active === true`), `list` (raw array of Controllers, safe to reorder), and convenience factories like `addBlur()`, `addGlow()`, `addMask()`. + +Every filter is a `Phaser.Filters.Controller` subclass. Common properties: `active` (toggle on/off without removing), `camera` (the owning camera), `renderNode` (shader render node ID), `paddingOverride` (override automatic padding), `ignoreDestroy` (if true the filter survives when its FilterList is destroyed, for reuse). Key methods: `setActive(bool)`, `setPaddingOverride(left, top, right, bottom)`, `getPadding()`, `destroy()`. + +Cameras have filters by default; game objects do not — you must call `enableFilters()` first: +```js +const sprite = this.add.sprite(400, 300, 'hero'); +sprite.enableFilters(); // creates an internal filterCamera, returns this for chaining +sprite.filters.internal.addGlow(); +sprite.filters.external.addVignette(); +``` +After enabling, related game-object properties: `filterCamera` (the internal camera used for filter rendering), `filters` ({internal, external} FilterList pair), `renderFilters` (master toggle, default true), `filtersAutoFocus` (auto-adjust camera to follow the object, default true), `filtersFocusContext` (focus the rendering context instead of object bounds, default false), `filtersForceComposite` (always draw to a framebuffer even with no active filters, default false), `maxFilterSize` (max texture size for filter framebuffers). Use `willRenderFilters()` to check if any active filters will actually render. +</core_concepts> + +<common_patterns> +Adding and modifying filters on game objects: +```js +const sprite = this.add.sprite(400, 300, 'enemy'); +sprite.enableFilters(); + +const glow = sprite.filters.internal.addGlow(0x00ff00, 4); +glow.outerStrength = 8; // modify at runtime +glow.color = 0xff0000; +glow.setActive(false); // temporarily disable +sprite.filters.internal.remove(glow); // remove and destroy +``` + +Camera filters: +```js +const camera = this.cameras.main; +const blur = camera.filters.internal.addBlur(0, 2, 2, 1); // camera-local space +const vignette = camera.filters.external.addVignette(0.5, 0.5, 0.5, 0.5); // screen space +const cm = camera.filters.internal.addColorMatrix(); +cm.colorMatrix.sepia(); // color grading +``` + +Chaining — filters execute in list order; each receives the output of the previous one: +```js +const cam = this.cameras.main; +const cm = cam.filters.internal.addColorMatrix(); +cm.colorMatrix.brightness(0.2); // first: color grading +cam.filters.internal.addBlur(1, 2, 2, 1); // second: blur the color-graded result +cam.filters.external.addVignette(0.5, 0.5, 0.5, 0.8); // third: vignette on top +``` + +Masks in v4 are implemented as filters — they use a texture's or game object's alpha channel to control visibility. Internal masks match the object being filtered; external masks match the camera context. Use a `viewCamera` parameter to control which camera renders the mask game object: +```js +sprite.enableFilters(); +sprite.filters.internal.addMask('maskTexture'); // static texture + +const maskShape = this.add.circle(0, 0, 100, 0xffffff); +const mask = sprite.filters.internal.addMask(maskShape); // game object (renders to DynamicTexture) +mask.invert = true; +mask.autoUpdate = true; // default: re-renders each frame +mask.needsUpdate = true; // force a one-time update +sprite.filters.external.addMask(maskShape, false, this.cameras.main); // specific viewing camera +``` + +Wipe / reveal transitions — animate `progress` via tween: +```js +const wipe = camera.filters.external.addWipe(0.1, 0, 0); +this.tweens.add({ targets: wipe, progress: 1, duration: 2000, ease: 'Linear' }); +wipe.setLeftToRight(); +wipe.setTopToBottom(); +wipe.setRevealEffect(); // reveal mode +wipe.setWipeEffect(); // wipe mode +wipe.setTexture('nextSceneCapture'); // wipe to another texture (scene transitions) +``` + +ParallelFilters splits the input into two paths, processes each independently, then blends — this replaces the dedicated Bloom filter from v3: +```js +const pf = camera.filters.internal.addParallelFilters(); +pf.top.addThreshold(0.5, 1); // top path: threshold bright areas... +pf.top.addBlur(); // ...then blur them +pf.blend.blendMode = Phaser.BlendModes.ADD; // configure how top combines onto bottom +pf.blend.amount = 0.5; +// Bottom path: left empty = uses original input +``` + +CaptureFrame captures the current render state at the point it appears in the display list — objects rendered before it are captured, objects after are not (requires composite mode on the camera): +```js +this.cameras.main.setForceComposite(true); +const bg = this.add.image(400, 300, 'background'); // rendered before CaptureFrame -> captured +const capture = this.add.captureFrame('myCapture'); +const display = this.add.image(400, 300, 'myCapture'); // display the captured texture with filters +display.enableFilters(); +display.filters.internal.addBlur(0, 4, 4, 2); +``` +</common_patterns> + +<built_in_filters> +All accessible via FilterList factory methods (`addX(...)`): +- **Barrel** `addBarrel(amount)` — pinch/expand distortion; `amount=1` is neutral. +- **Blend** `addBlend(texture, blendMode, amount, color)` — blend another texture using a blend mode (supports modes not available in standard WebGL). +- **Blocky** `addBlocky(config)` — pixelation that preserves original colors (no blending); best without anti-aliasing. +- **Blur** `addBlur(quality, x, y, strength, color, steps)` — Gaussian blur; quality 0=low, 1=medium, 2=high. +- **Bokeh** `addBokeh(radius, amount, contrast)` — depth-of-field bokeh blur. +- **ColorMatrix** `addColorMatrix()` — color manipulation via matrix; access `.colorMatrix` for presets. +- **CombineColorMatrix** `addCombineColorMatrix(texture)` — combine channels from two textures via color matrices (e.g. alpha transfer). +- **Displacement** `addDisplacement(texture, x, y)` — pixel displacement via a displacement map; values are very small floats (e.g. 0.005). +- **Glow** `addGlow(color, outerStrength, innerStrength, scale, knockout, quality, distance)` — luminous halo around edges; supports inner/outer glow and knockout mode. +- **GradientMap** `addGradientMap(config)` — recolor image using a ColorRamp based on brightness. +- **ImageLight** `addImageLight(config)` — image-based lighting using a panorama environment map and normal map. +- **Key** `addKey(config)` — chroma key: remove or isolate a color. Config `{ color, threshold, feather, isolate }`. +- **Mask** `addMask(mask, invert, viewCamera, viewTransform, scaleFactor)` — alpha masking via texture or game object. +- **NormalTools** `addNormalTools(config)` — manipulate normal maps: rotate, adjust facing power, output grayscale facing data. +- **PanoramaBlur** `addPanoramaBlur(config)` — spherically-correct blur for panorama images (for use with ImageLight; very slow). +- **ParallelFilters** `addParallelFilters()` — split input into two filter paths, blend results; use for custom bloom. +- **Pixelate** `addPixelate(amount)` — mosaic/pixelation; pixel size = 2 + amount; blends colors (unlike Blocky). +- **Quantize** `addQuantize(config)` — reduce color palette; supports RGBA/HSVA modes, gamma, offset, dithering. +- **Sampler** `addSampler(callback, region)` — extract pixel data from the render; does not alter the image; expensive. +- **Shadow** `addShadow(x, y, decay, power, color, samples, intensity)` — drop shadow with offset, decay, and color. +- **Threshold** `addThreshold(edge1, edge2, invert)` — binary threshold per channel; edges can be arrays for per-channel control. +- **TiltShift** `addTiltShift(radius, amount, contrast, blurX, blurY, strength)` — miniature/tilt-shift effect (uses Bokeh internally). +- **Vignette** `addVignette(x, y, radius, strength, color, blendMode)` — edge darkening/coloring; supports NORMAL, ADD, MULTIPLY, SCREEN blend modes. +- **Wipe** `addWipe(wipeWidth, direction, axis, reveal, wipeTexture)` — wipe/reveal transition; animate `progress` via tween. + +Mask filter API: +```js +const mask = list.addMask('texKey'); // from texture key +const mask = list.addMask(gameObject); // from game object +mask.invert = true; +mask.autoUpdate = false; // stop auto-updating GO masks +mask.needsUpdate = true; // force one update +mask.setTexture('newKey'); +mask.setGameObject(newGO); +mask.viewCamera = otherCamera; // camera for GO rendering +mask.viewTransform = 'local'; // 'local' or 'world' +mask.scaleFactor = 0.5; // scale mask texture size +``` + +ColorMatrix presets (call on `addColorMatrix().colorMatrix`): `sepia()`, `grayscale(1)`, `brightness(0.3)`, `hue(90)`, `saturate(-0.5)`, `contrast(0.3)`, `blackWhite()`, `negative()`, `desaturate()`, `night(0.5)`, `lsd()`, `brown()`, `vintagePinhole()`, `kodachrome()`, `technicolor()`, `polaroid()`, `shiftToBGR()`. +</built_in_filters> + +<gotchas> +1. **WebGL only.** Filters do not work in the Canvas renderer. `enableFilters()` returns early if WebGL is unavailable. + +2. **`enableFilters()` required for game objects.** Cameras have filters by default; sprites, images, containers, and other game objects require `enableFilters()` before accessing `filters`. + +3. **Performance cost.** Each object with active filters creates extra draw calls (one for the base render plus one per active filter). Use sparingly and performance test early. + +4. **Internal vs external matters.** Internal filters are cheaper (object-region sized); external filters are full-screen. A blur that should rotate with the object must be internal; a blur that should stay screen-aligned must be external. + +5. **Filter order matters.** Filters are applied sequentially in list order — the output of one feeds into the next. + +6. **Glow quality and distance are immutable.** `quality` and `distance` on the Glow filter cannot be changed after creation. Destroy and recreate the filter to change them. + +7. **CaptureFrame requires forceComposite.** The camera must have `setForceComposite(true)` or otherwise render into a framebuffer for CaptureFrame to work. + +8. **Padding for expanding effects.** Blur, Glow, and Shadow can automatically calculate padding to expand the render texture. Override with `setPaddingOverride()` (pass `null` to clear). On a camera, use `camera.getPaddingWrapper(x)` to render more world outside the image edge. + +9. **Controller reuse.** By default controllers are destroyed when their FilterList is destroyed. Set `ignoreDestroy = true` to reuse a controller across multiple objects, but then you must manage its lifecycle manually. Works best with external filters. + +10. **Mask game object rendering.** When using a game object as a mask source, it renders to a DynamicTexture each frame (if `autoUpdate` is true). For static masks, set `autoUpdate = false` and use `needsUpdate = true` for one-shot updates to improve performance. + +11. **No Bloom filter.** v4 has no dedicated Bloom filter. Use ParallelFilters with Threshold + Blur + ADD blend (see Common Patterns), or use `Phaser.Actions.AddEffectBloom` to automate it. +</gotchas> + +<v4_changes_from_v3> +- `gameObject.preFX` / `gameObject.postFX` → `gameObject.filters.internal` / `gameObject.filters.external`. +- `camera.postFX` → `camera.filters.internal` / `camera.filters.external` (cameras now have both lists). +- `FX.addBloom()` → use ParallelFilters + Threshold + Blur, or `Phaser.Actions.AddEffectBloom`. No dedicated Bloom filter. +- `FX.addCircle()` → use Vignette (with radius), a circular Mask, or `Phaser.Actions.AddMaskShape`. +- `FX.addGradient()` → use the new Gradient GameObject; add Quantize for steps if wanted. +- Glow `quality` was a 0-1 fraction; it is now an integer (default 10), with stochastic sampling replacing line sampling (higher quality at lower values). +- `camera.setMask()` → `camera.filters.internal.addMask()`; `gameObject.setMask()` → `gameObject.filters.internal.addMask()`. Masks are now filters, not a separate system. +- FX controllers → `Phaser.Filters.Controller` subclasses (same pattern: returned controller objects with mutable properties). +- New in v4: `enableFilters()` is now a required explicit opt-in for game objects; new filters Blocky, Quantize, Key, Blend, CombineColorMatrix, ImageLight, NormalTools, PanoramaBlur, ParallelFilters, Sampler. +</v4_changes_from_v3> +</filters_and_postfx> diff --git a/skills/base44-platform-games/references/phaser/game_object_components.md b/skills/base44-platform-games/references/phaser/game_object_components.md new file mode 100644 index 0000000..ffa8524 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/game_object_components.md @@ -0,0 +1,158 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: game object components + +<game_object_components> +Use this when working with Phaser 4 game object components and the mixin system: Transform, Alpha, Tint, Origin, Depth, Flip, Mask, GetBounds, Lighting, ScrollFactor, Size, Texture, Visible, PathFollower, and the v4 filter components. + +<quick_start> +Every Game Object gets its component methods via mixins — just call them directly on any game object: + +```js +const sprite = this.add.sprite(400, 300, 'hero'); + +sprite.setAlpha(0.5); // Alpha +sprite.setBlendMode(Phaser.BlendModes.ADD); // BlendMode +sprite.setDepth(10); // Depth +sprite.setFlip(true, false); // Flip +sprite.setOrigin(0, 0); // Origin +sprite.setScrollFactor(0); // ScrollFactor (HUD) +sprite.setDisplaySize(64, 64); // Size +sprite.setTexture('hero', 'walk-1'); // Texture +sprite.setTint(0xff0000); // Tint +sprite.setPosition(100, 200); // Transform +sprite.setVisible(false); // Visible +sprite.setScale(2); // Transform +sprite.setAngle(45); // Transform + +sprite.setAlpha(1, 0.5, 0.5, 1); // per-corner alpha (WebGL only) +sprite.setCrop(0, 0, 32, 32); // crop a texture region + +// Enable WebGL filters (post-processing) +sprite.enableFilters(); +sprite.filters.internal.addBlur(1, 2, 2, 1); + +sprite.setLighting(true); // Lighting (WebGL only, v4 feature) +``` +</quick_start> + +<core_concepts> +Phaser uses `Phaser.Class` with a `Mixins` array to compose Game Objects from reusable component objects. Each component is a plain JS object whose properties and methods are copied onto the class prototype at class-definition time — this is property copying, NOT prototypal inheritance. Components are mixed in per-class, not per-instance. + +```js +// How Phaser defines Sprite internally: +var Sprite = new Class({ + Extends: GameObject, + Mixins: [ + Components.Alpha, Components.BlendMode, Components.Depth, + Components.Flip, Components.GetBounds, Components.Lighting, + Components.Mask, Components.Origin, Components.RenderNodes, + Components.ScrollFactor, Components.Size, Components.TextureCrop, + Components.Tint, Components.Transform, Components.Visible, + SpriteRender + ], + initialize: function Sprite (scene, x, y, texture, frame) { /* ... */ } +}); +``` + +Key rules: +- Most setter methods return `this` for chaining: `sprite.setAlpha(0.5).setDepth(10).setPosition(100, 200)`. +- Many properties use getters/setters internally (`alpha`, `depth`, `visible`, `scale`, `rotation`). Setting `alpha = 0` or `scale = 0` clears render flags, skipping rendering entirely. +- `renderFlags` is a bitmask: bit 0 = visible, bit 1 = alpha, bit 2 = scale, bit 3 = texture. All bits must be set for the object to render. +- **Alpha vs AlphaSingle**: `Alpha` supports per-corner alpha (WebGL); `AlphaSingle` only supports a single global alpha. Containers use `AlphaSingle`. +- **Texture vs TextureCrop**: `TextureCrop` extends `Texture` with `setCrop()` support. Sprites and Images use `TextureCrop`; some objects use plain `Texture`. +- **Filters and RenderSteps** are v4-only, WebGL-only systems for post-processing. +</core_concepts> + +<components> +**Alpha** — per-corner transparency (Sprite, Image, Text, most renderables). `alpha` (0-1, setting also sets all four corners; 0 disables rendering), `alphaTopLeft`/`alphaTopRight`/`alphaBottomLeft`/`alphaBottomRight` (WebGL only), `setAlpha(topLeft?, topRight?, bottomLeft?, bottomRight?)` (one arg = uniform, four = per-corner), `clearAlpha()` (resets to 1). + +**AlphaSingle** — simplified alpha, no per-corner support (used by Container). `alpha` (0-1), `setAlpha(value?)`, `clearAlpha()`. + +**BlendMode** — how pixels composite. `blendMode` (accepts `Phaser.BlendModes` const, string, or integer), `setBlendMode(value)`. WebGL modes: `NORMAL`, `ADD`, `MULTIPLY`, `SCREEN`, `ERASE`. Canvas supports additional browser-dependent modes. + +**Depth** — rendering order (z-index); higher renders on top. `depth` (setting queues a depth sort, default 0), `setDepth(value)`, `setToTop()`, `setToBack()`, `setAbove(gameObject)`, `setBelow(gameObject)`. + +**Flip** — visual mirroring without affecting physics bodies. `flipX`/`flipY` (default false), `setFlipX(value)`, `setFlipY(value)`, `setFlip(x, y)`, `toggleFlipX()`, `toggleFlipY()`, `resetFlip()`. + +**GetBounds** — positions and bounding rects regardless of origin. `getCenter`, `getTopLeft`, `getTopCenter`, `getTopRight`, `getLeftCenter`, `getRightCenter`, `getBottomLeft`, `getBottomCenter`, `getBottomRight` (all `(output?, includeParent?)` returning `Vector2Like`), `getBounds(output?)` returning `Rectangle`. Pass `includeParent = true` to factor in parent Container transforms. + +**Lighting** (v4, WebGL only) — light-based rendering. `lighting` (default false), `selfShadow` (`{ enabled, penumbra, diffuseFlatThreshold }`), `setLighting(enable)`, `setSelfShadow(enabled?, penumbra?, diffuseFlatThreshold?)` (pass `null` for enabled to use game config default). + +**Mask** (Canvas only in v4) — geometry masking for the Canvas renderer; in WebGL use `Filters.addMask()` instead. `mask` (current GeometryMask or null), `setMask(mask)` (Canvas only; logs a warning in WebGL), `clearMask(destroyMask?)`, `createGeometryMask(graphics?)`. + +**Origin** — anchor point for position/rotation/scale, normalized 0-1. `originX`/`originY` (default 0.5 = center), `displayOriginX`/`displayOriginY` (pixels; setting recalculates normalized origin), `setOrigin(x?, y?)` (y defaults to x), `setOriginFromFrame()`, `setDisplayOrigin(x?, y?)`, `updateDisplayOrigin()`. + +**ScrollFactor** — how camera scrolling affects rendered position. `scrollFactorX`/`scrollFactorY` (default 1), `setScrollFactor(x, y?)`. Key values: `0` = fixed to camera (HUD), `1` = moves with camera, `0.5` = parallax half-speed. + +**Size** — native and display dimensions. `width`/`height` (native, un-scaled), `displayWidth`/`displayHeight` (scaled; setting adjusts scaleX/scaleY), `setSize(width, height)` (native size, does NOT affect rendered size), `setDisplaySize(width, height)` (rendered size via scale), `setSizeToFrame(frame?)`. + +**Texture** — texture and frame for rendering. `texture`, `frame`, `setTexture(key, frame?, updateSize?, updateOrigin?)`, `setFrame(frame, updateSize?, updateOrigin?)` (name, index, or Frame instance; updates size and origin by default). + +**TextureCrop** — Texture plus cropping (Sprite, Image). Adds `isCropped` and `setCrop(x?, y?, width?, height?)` (no args clears; accepts a `Rectangle` as first arg). `setFrame` also updates crop UVs when cropped. + +**Tint** (WebGL only) — color tinting via per-corner vertex colors. `tint` (uniform, default 0xffffff), `tintTopLeft`/`tintTopRight`/`tintBottomLeft`/`tintBottomRight`, `tintMode` (default `Phaser.TintModes.MULTIPLY`), `isTinted` (readonly), `setTint(topLeft?, topRight?, bottomLeft?, bottomRight?)`, `setTintMode(mode)` (`MULTIPLY`, `FILL`, `ADD`, `SCREEN`, `OVERLAY`, `HARD_LIGHT`), `clearTint()`. v4 change: `setTintFill()` is removed — use `setTint(color).setTintMode(Phaser.TintModes.FILL)`. + +**Transform** — position, scale, rotation, coordinate transforms. `x`/`y`/`z`/`w` (`z` is NOT rendering order — use `depth`), `scale` (get returns average of scaleX/scaleY), `scaleX`/`scaleY` (default 1), `angle` (degrees, clockwise, 0 = right), `rotation` (radians). Methods: `setPosition(x?, y?, z?, w?)`, `copyPosition(source)`, `setRandomPosition(x?, y?, width?, height?)`, `setRotation(radians?)`, `setAngle(degrees?)`, `setScale(x?, y?)`, `setX`/`setY`/`setZ`/`setW`, `getLocalTransformMatrix(tempMatrix?)`, `getWorldTransformMatrix(tempMatrix?, parentMatrix?)`, `getLocalPoint(x, y, point?, camera?)`, `getWorldPoint(point?, tempMatrix?, parentMatrix?)`, `getParentRotation()`. + +**Visible** — whether the object renders (invisible objects still run update logic). `visible` (default true), `setVisible(value)`. + +**PathFollower** — follow a `Phaser.Curves.Path` via an internal tween. `path`, `rotateToPath` (default false), `pathRotationOffset`, `pathOffset`, `pathVector`, `pathDelta`, `pathTween`. Methods: `setPath(path, config?)`, `setRotateToPath(value, offset?)`, `isFollowing()`, `startFollow(config?, startAt?)` (config can be a duration number or a PathConfig), `pauseFollow()`, `resumeFollow()`, `stopFollow()`. + +**RenderNodes, RenderSteps, Filters, FilterList** (v4, WebGL only) — call `enableFilters()` on game objects before accessing `filters`. Cameras have filters by default. Use `filters.internal` for object-local effects and `filters.external` for screen-space effects. +</components> + +<factory_and_display_list> +Register custom game objects on the factory so they can be created via `this.add.myObject()`: + +```js +class LaserBeam extends Phaser.GameObjects.Sprite { + constructor(scene, x, y) { super(scene, x, y, 'laser'); } +} + +Phaser.GameObjects.GameObjectFactory.register('laserBeam', function (x, y) { + const beam = new LaserBeam(this.scene, x, y); + this.displayList.add(beam); + return beam; +}); + +const laser = this.add.laserBeam(100, 200); // usable in any scene +``` + +**Factory (`this.add`) vs Creator (`this.make`)**: `this.add` creates AND adds to the display list. `this.make` creates but does NOT add — useful for off-screen prep or deferred display. + +```js +const visible = this.add.sprite(100, 100, 'hero'); // on display list +const offscreen = this.make.sprite({ key: 'hero', x: 100, y: 100 }); // not added +this.add.existing(offscreen); // add later +``` + +`GetAdvancedValue` resolves static values, random arrays, randInt/randFloat ranges, and callbacks from config objects: + +```js +{ speed: 100 } // static value +{ speed: [100, 200, 300] } // random from array +{ speed: { randInt: [50, 150] } } // random integer range +{ speed: { randFloat: [0.5, 1.5] } } // random float range +{ speed: function () { return Math.random() * 100; } } // callback +``` + +Display list ordering: objects added later render on top. Control with `setDepth(10)` (higher = on top, triggers a sort) or display-list position (`setToTop()`, `setToBack()`, `setAbove(otherSprite)`, `setBelow(otherSprite)`). When both are used, `depth` takes priority after the next sort pass. +</factory_and_display_list> + +<gotchas> +- **alpha=0 prevents rendering entirely**: setting `alpha` to 0 clears a render flag bit — the object is not drawn at all, not merely transparent. Same for `scale = 0` and `visible = false`. +- **Flip does not affect physics**: flipping is rendering-only; physics bodies remain unchanged. Account for this in collision code. +- **ScrollFactor and physics**: collisions use world position. A scroll factor other than 1 offsets where the texture renders but not where the body is. For HUD elements with scroll factor 0, avoid adding physics bodies. +- **depth vs display list order**: `setDepth()` queues a sort and is the primary z-ordering tool. `setToTop()`/`setAbove()`/etc. reorder the display list without changing `depth`. If you mix both, `depth` wins after the next sort. +- **Mask component is Canvas-only in v4**: for WebGL masking, use `enableFilters()` then `filters.internal.addMask()`. `setMask()` logs a warning in WebGL. +- **setTintFill is removed in v4**: use `setTint(color).setTintMode(Phaser.TintModes.FILL)`. +- **Tint mode is separate from tint color in v4**: modes are `MULTIPLY` (default), `FILL`, `ADD`, `SCREEN`, `OVERLAY`, `HARD_LIGHT`. +- **displayWidth/displayHeight adjusts scale**: setting them modifies `scaleX`/`scaleY`, NOT `width`/`height`. +- **setSize vs setDisplaySize**: `setSize()` changes internal dimensions (frames, physics); `setDisplaySize()` changes rendered size by adjusting scale. +- **Origin default is 0.5**: all Game Objects default to center origin. Use `setOrigin(0, 0)` for top-left positioning. Text and BitmapText instead default to top-left. +- **Transform.z is NOT depth**: `z` is for custom use. Rendering order is `depth` or display list position. +- **Filters are expensive**: each object with active filters needs extra draw calls. Use `renderFilters = false` to temporarily disable. Internal filters are cheaper than external. +</gotchas> +</game_object_components> diff --git a/skills/base44-platform-games/references/phaser/game_setup_and_config.md b/skills/base44-platform-games/references/phaser/game_setup_and_config.md new file mode 100644 index 0000000..b4c0a9a --- /dev/null +++ b/skills/base44-platform-games/references/phaser/game_setup_and_config.md @@ -0,0 +1,233 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: game setup and config + +<game_setup_and_config> +Use this when creating a new Phaser 4 game instance or configuring GameConfig options: renderer selection, canvas setup, scaling, pixel art, FPS settings, boot sequence, and config sub-objects. + +<quick_start> +The simplest possible Phaser 4 game — a single scene with the default 1024×768 canvas: + +```js +class MyScene extends Phaser.Scene { + preload() { this.load.image('logo', 'assets/logo.png'); } + create() { this.add.image(400, 300, 'logo'); } +} + +const config = { + type: Phaser.AUTO, + scene: MyScene +}; + +const game = new Phaser.Game(config); +``` + +`new Phaser.Game(config)` triggers the entire boot sequence: config parsing, renderer creation, DOM insertion, and the game loop (`TimeStep`). The game waits for `DOMContentLoaded` before booting. +</quick_start> + +<core_concepts> +**Boot sequence**: `new Phaser.Game(config)` parses config into a `Phaser.Core.Config` instance, then creates the global managers (`AnimationManager`, `TextureManager`, `CacheManager`, `InputManager`, `SceneManager`, `ScaleManager`, `SoundManager`, `TimeStep`, `PluginManager`). After `DOMContentLoaded` it calls `boot()` — which creates the renderer, adds the canvas to the DOM, prints the debug header, and emits `BOOT`. Once textures are ready (`TextureManager` emits `READY`), it emits `READY` and calls `start()`, which begins the `TimeStep` loop, sets up the `VisibilityHandler`, and calls `config.postBoot`. + +**Config parsing**: the `Config` constructor reads a flat `GameConfig` object and resolves defaults. Some properties can be specified at top level OR nested in sub-objects (e.g. `width` can be top-level or under `scale.width`; `pixelArt` can be top-level or under `render`). The nested sub-object (`scale`, `render`) takes priority when both are present. + +**Renderer constants** (set via `config.type`, default `Phaser.AUTO`): +- `Phaser.AUTO` (0) — WebGL if supported, else falls back to Canvas. +- `Phaser.CANVAS` (1) — force Canvas renderer. +- `Phaser.WEBGL` (2) — force WebGL, no fallback if unsupported. +- `Phaser.HEADLESS` (3) — no renderer (DOM still required). For unit testing only. +</core_concepts> + +<common_patterns> +Pixel art game — when `pixelArt` is `true`, Config automatically sets `antialias: false`, `antialiasGL: false`, and `roundPixels: true`: + +```js +const config = { + type: Phaser.AUTO, + width: 320, + height: 240, + pixelArt: true, + scale: { + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_BOTH, + zoom: Phaser.Scale.ZOOM_2X + }, + scene: MyScene +}; +``` + +Smooth pixel art (WebGL only) — preserves blocky pixels but smooths edges between them when scaled up. When `smoothPixelArt` is `true`, Config sets `antialias: true`, `antialiasGL: true`, and `pixelArt: false`: + +```js +const config = { + type: Phaser.WEBGL, + width: 320, + height: 240, + smoothPixelArt: true, + scene: MyScene +}; +``` + +Full-window responsive game: + +```js +const config = { + type: Phaser.AUTO, + scale: { + mode: Phaser.Scale.RESIZE, + parent: 'game-container', + width: '100%', + height: '100%' + }, + scene: MyScene +}; +``` + +Fixed aspect ratio with FIT (letterboxing): + +```js +const config = { + type: Phaser.AUTO, + scale: { + mode: Phaser.Scale.FIT, + parent: 'game-container', + autoCenter: Phaser.Scale.CENTER_BOTH, + width: 1280, + height: 720, + min: { width: 640, height: 360 }, + max: { width: 1920, height: 1080 } + }, + backgroundColor: '#2d2d2d', + scene: MyScene +}; +``` + +Custom FPS limit: + +```js +const config = { + type: Phaser.AUTO, + width: 800, + height: 600, + fps: { target: 60, limit: 30, forceSetTimeOut: false, smoothStep: true }, + scene: MyScene +}; +``` + +Transparent canvas over HTML — when `transparent` is `true`, `backgroundColor` is forced to `0x000000` with alpha `0`: + +```js +const config = { + type: Phaser.AUTO, + width: 800, + height: 600, + transparent: true, + parent: 'game-container', + scene: MyScene +}; +``` + +Pre-existing canvas element: + +```js +const canvas = document.getElementById('my-canvas'); +const config = { + type: Phaser.WEBGL, + canvas: canvas, + width: 800, + height: 600, + scene: MyScene +}; +``` + +Multiple scenes — only the first scene starts automatically; others start only if they have `{ active: true }` in their scene config: + +```js +const config = { + type: Phaser.AUTO, + width: 800, + height: 600, + scene: [BootScene, PreloadScene, MenuScene, GameScene], + physics: { default: 'arcade', arcade: { gravity: { y: 300 } } } +}; +``` + +Boot callbacks: + +```js +const config = { + type: Phaser.AUTO, + width: 800, + height: 600, + callbacks: { + preBoot: function (game) { /* before boot; game systems not yet available */ }, + postBoot: function (game) { /* after boot; all systems ready, loop starting */ } + }, + scene: MyScene +}; +``` + +Disabling input subsystems: + +```js +const config = { + type: Phaser.AUTO, + width: 800, + height: 600, + input: { + keyboard: true, mouse: true, touch: false, + gamepad: false, activePointers: 1, windowEvents: true + }, + disableContextMenu: true, + scene: MyScene +}; +``` + +**Scale modes** (`scale.mode`): `NONE` (0, no scaling), `WIDTH_CONTROLS_HEIGHT` (1), `HEIGHT_CONTROLS_WIDTH` (2), `FIT` (3, fit inside parent keeping aspect ratio — letterbox), `ENVELOP` (4, cover parent keeping aspect ratio — may overflow), `RESIZE` (5, fill parent ignoring aspect ratio), `EXPAND` (6, RESIZE for visible area + FIT for canvas scale). **Center modes** (`scale.autoCenter`): `NO_CENTER` (0), `CENTER_BOTH` (1), `CENTER_HORIZONTALLY` (2), `CENTER_VERTICALLY` (3). **Zoom constants** (`scale.zoom`): `NO_ZOOM` (1), `ZOOM_2X` (2), `ZOOM_4X` (4), `MAX_ZOOM` (-1, max integer zoom that fits in parent). +</common_patterns> + +<lifecycle_and_globals> +Listen on `game.events` (or `this.game.events` from a Scene) for visibility and focus changes (use named constants `Phaser.Core.Events.BLUR`, `FOCUS`, `HIDDEN`, `VISIBLE`, `PAUSE`, `RESUME`): + +```js +game.events.on('blur', () => { /* tab lost focus */ }); +game.events.on('focus', () => { /* tab regained focus */ }); +game.events.on('hidden', () => { /* Page Visibility API: page hidden */ }); +game.events.on('visible', () => { /* Page Visibility API: page visible */ }); +game.events.on('pause', () => { /* game.pause() was called */ }); +game.events.on('resume', () => { /* game.resume() was called */ }); +``` + +Pause, resume, and destroy: + +```js +game.pause(); game.resume(); +if (game.isPaused) { /* game loop is not running */ } + +game.destroy(true); // remove canvas from DOM and clean up +game.destroy(true, true); // noReturn=true: NEVER creating another instance +``` + +`destroy(removeCanvas, noReturn)` is asynchronous — it flags the game for destruction on the next frame. Listen for `Phaser.Core.Events.DESTROY` to react to completion. + +The `Game` instance exposes key managers as properties, accessible from any Scene via `this.game`: `game.config` (parsed Config, read-only after boot), `game.scene` (SceneManager — start/stop/switch scenes), `game.registry` (DataManager — shared runtime data manager across all scenes), `game.anims` (global animation definitions), `game.cache` (loaded non-texture assets), `game.textures` (loaded textures and sprite sheets), `game.sound` (global sound manager). + +```js +this.game.registry.set('highScore', 9999); // shared registry from any scene +this.game.scene.start('MenuScene'); // scene manager +``` +</lifecycle_and_globals> + +<gotchas> +- **scale vs top-level config**: `width`, `height`, `zoom`, and `parent` can be set at top level or inside `scale`; the `scale` sub-object wins. Avoid setting both. +- **Phaser.WEBGL has no fallback**: unlike `AUTO`, it will not fall back to Canvas if WebGL is unavailable — the game fails silently. +- **parent: null vs parent: undefined**: `undefined` (or omitted) appends the canvas to `document.body`. `null` means Phaser does not add the canvas to the DOM at all — you must do it yourself. +- **transparent overrides backgroundColor**: when `transparent: true`, the background is forced to `rgba(0,0,0,0)` regardless of what you set. +- **fps.target does not enforce frame rate**: it is advisory only. Use `fps.limit` to actually cap. The browser's display refresh rate is always the upper bound. +- **fps.limit only slows down, never speeds up**: `limit: 120` on a 60Hz display still results in 60 FPS. +- **DOM Container requires a parent**: `dom.createContainer: true` without a `parent` element will not work. +- **smoothPixelArt is WebGL-only**: no effect with the Canvas renderer. +- **window.FORCE_WEBGL / window.FORCE_CANVAS**: Config.js checks these globals at the end of parsing and overrides the `type` setting (intended for development/testing). +- **Game.destroy() is asynchronous**: it flags destruction on the next frame; listen for the `DESTROY` event. Pass `noReturn: true` only if you will never create another Phaser instance on the same page. +</gotchas> +</game_setup_and_config> diff --git a/skills/base44-platform-games/references/phaser/geometry_and_math.md b/skills/base44-platform-games/references/phaser/geometry_and_math.md new file mode 100644 index 0000000..1278033 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/geometry_and_math.md @@ -0,0 +1,156 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: geometry and math + +<geometry_and_math> +Use this when using Phaser 4 math and geometry utilities: Geom classes (Circle, Ellipse, Line, Polygon, Rectangle, Triangle), intersection tests, vectors and matrices, angles, distance, interpolation, easing, random number generation, snapping, clamping, and color utilities. + +<quick_start> +```js +// Geometry objects are data only — not renderable +const rect = new Phaser.Geom.Rectangle(10, 20, 200, 100); +const circle = new Phaser.Geom.Circle(400, 300, 50); +const line = new Phaser.Geom.Line(0, 0, 100, 100); + +rect.contains(50, 50); // true — point containment +circle.contains(410, 310); // true +Phaser.Geom.Intersects.CircleToRectangle(circle, rect); // boolean + +// Math utilities +const v = new Phaser.Math.Vector2(3, 4); +v.length(); // 5 +v.normalize(); // {x: 0.6, y: 0.8} + +const dist = Phaser.Math.Distance.Between(0, 0, 100, 100); +const angle = Phaser.Math.Angle.Between(0, 0, 100, 100); +const clamped = Phaser.Math.Clamp(150, 0, 100); // 100 +``` +</quick_start> + +<core_concepts> +Geometry objects are pure data containers holding coordinates and dimensions. They are NOT game objects and cannot be added to the display list — to render geometry, use the Graphics game object or Shape game objects. Every geom class has a `type` property set to a `Phaser.Geom` constant for fast type checks. + +All geom classes share a common instance method pattern: `contains(x, y)` (point-in-shape), `getPoint(position, output?)` (point at normalized 0-1 position on perimeter), `getPoints(quantity, stepRate?, output?)` (evenly spaced perimeter points), `getRandomPoint(output?)` (random point inside), `setTo(...)`, `setEmpty()`, `setPosition(x, y)`. Each geom type also has a folder of static helper functions (e.g. `Phaser.Geom.Rectangle.Contains`, `Phaser.Geom.Circle.Area`). +</core_concepts> + +<geometry_classes> +Constructors and key properties: +- **Circle** `Phaser.Geom.Circle(x, y, radius)` — `x`, `y`, `radius`, `diameter`, `left`, `right`, `top`, `bottom`. +- **Ellipse** `Phaser.Geom.Ellipse(x, y, width, height)` — `x`, `y`, `width`, `height`, edges. +- **Line** `Phaser.Geom.Line(x1, y1, x2, y2)` — `x1`, `y1`, `x2`, `y2`, edges. +- **Polygon** `Phaser.Geom.Polygon(points)` — `area`, `points` (array of Vector2Like). +- **Rectangle** `Phaser.Geom.Rectangle(x, y, width, height)` — `x`, `y`, `width`, `height`, edges, `centerX`, `centerY`. +- **Triangle** `Phaser.Geom.Triangle(x1, y1, x2, y2, x3, y3)` — three vertex pairs. + +The Polygon constructor accepts multiple `points` formats: space-separated string (`'40 0 40 20 100 20'`), array of `{x, y}` objects, flat number array (`[x1, y1, x2, y2, ...]`), or array of `[x, y]` sub-arrays. + +Each type has a rich set of static helpers under `Phaser.Geom.<Type>.*` — e.g. Rectangle: Area, CenterOn, Contains, ContainsRect, FitInside, FitOutside, Inflate, Intersection, MarchingAnts, Offset, Overlaps, Perimeter, Random, Scale, Union; Circle: Area, Circumference, CircumferencePoint, Contains, GetBounds, GetPoint, Random; Line: Angle, BresenhamPoints, Extend, GetMidPoint, GetNearestPoint, GetNormal, Length, Rotate, SetToAngle, Slope; Triangle: Area, BuildEquilateral, BuildRight, Centroid, CircumCenter, CircumCircle, InCenter, Rotate. +</geometry_classes> + +<intersection_tests> +All under `Phaser.Geom.Intersects`. + +Boolean tests (return `true`/`false`): `CircleToCircle(a, b)`, `CircleToRectangle(circle, rect)`, `LineToCircle(line, circle)`, `LineToLine(l1, l2, out?)` (writes point to `out`), `LineToRectangle(line, rect)`, `PointToLine(point, line, lineThickness?)`, `PointToLineSegment(point, line)`, `RectangleToRectangle(a, b)`, `RectangleToTriangle(rect, tri)`, `RectangleToValues(rect, left, right, top, bottom, tolerance?)`, `TriangleToCircle(tri, circle)`, `TriangleToLine(tri, line)`, `TriangleToTriangle(a, b)`. + +Get intersection points (return `Vector2[]`, accept an optional `out` array): `GetCircleToCircle`, `GetCircleToRectangle`, `GetLineToCircle`, `GetLineToLine` (single point), `GetLineToPoints`, `GetLineToPolygon` (closest intersection), `GetLineToRectangle`, `GetRaysFromPointToPolygon(x, y, polygon)`, `GetRectangleIntersection` (overlapping region), `GetRectangleToRectangle`, `GetRectangleToTriangle`, `GetTriangleToCircle`, `GetTriangleToLine`, `GetTriangleToTriangle`. +</intersection_tests> + +<math_functions> +All under `Phaser.Math` unless noted. + +**Constants**: `TAU` (PI * 2, v4 addition), `PI_OVER_2`, `EPSILON` (1.0e-6), `DEG_TO_RAD` (PI / 180), `RAD_TO_DEG` (180 / PI), `RND` (global `RandomDataGenerator`, seeded via game config `seed`). + +**Angle** (`Phaser.Math.Angle.*`): `Between(x1, y1, x2, y2)`, `BetweenPoints(p1, p2)`, `BetweenY` / `BetweenPointsY` (from vertical axis), `CounterClockwise(angle)`, `GetClockwiseDistance(from, to)`, `GetCounterClockwiseDistance`, `GetShortestDistance` (signed), `Normalize(angle)` (to [0, 2PI)), `Random()`, `RandomDegrees()`, `Reverse(angle)` (add PI), `RotateTo(current, target, lerp?)`, `ShortestBetween(a1, a2)` (degrees), `Wrap(angle)` (to (-PI, PI]), `WrapDegrees(angle)` (to (-180, 180]). + +**Distance** (`Phaser.Math.Distance.*`): `Between(x1, y1, x2, y2)` (Euclidean), `BetweenPoints(a, b)`, `BetweenPointsSquared`, `Chebyshev` (chessboard), `Power(x1, y1, x2, y2, pow)` (Minkowski), `Snake` (Manhattan/taxicab), `Squared` (squared Euclidean — avoids sqrt). + +**Interpolation** (`Phaser.Math.Interpolation.*`): `BezierInterpolation(v, k)`, `CatmullRomInterpolation(v, k)`, `CubicBezierInterpolation(t, p0, p1, p2, p3)`, `LinearInterpolation(v, k)`, `QuadraticBezierInterpolation(t, p0, p1, p2)`, `SmoothStepInterpolation(t, min, max)`, `SmootherStepInterpolation(t, min, max)`. + +**Snap** (`Phaser.Math.Snap.*`): `To(value, gap, start?, divide?)` (nearest increment), `Floor(...)` (down), `Ceil(...)` (up). + +**Fuzzy** (`Phaser.Math.Fuzzy.*`): `Equal(a, b, epsilon?)`, `LessThan`, `GreaterThan`, `Ceil(value, epsilon?)`, `Floor(value, epsilon?)`. + +**Easing** (`Phaser.Math.Easing.*`, used primarily by tweens via string names like `'Sine.easeOut'`): each type has `.In`, `.Out`, `.InOut` variants — Back, Bounce, Circular (`Circ`), Cubic, Elastic, Expo, Quadratic (`Quad`), Quartic (`Quart`), Quintic (`Quint`), Sine. No-variant types: `Linear`, `Stepped`. Power aliases: `Power0` = Linear, `Power1` = Quad.Out, `Power2` = Cubic.Out, `Power3` = Quart.Out, `Power4` = Quint.Out. Short names default to `.Out` (`'Quad'` = Quad.Out). + +**Core helpers** (directly on `Phaser.Math`): `Between(min, max)` (random int), `FloatBetween(min, max)` (random float), `Clamp(value, min, max)`, `Wrap(value, min, max)`, `Within(a, b, tolerance)`, `Percent(value, min, max, upperMax?)`, `FromPercent(percent, min, max)`, `DegToRad(degrees)`, `RadToDeg(radians)`, `Linear(p0, p1, t)` (lerp), `LinearXY(v1, v2, t, out?)`, `SmoothStep(x, min, max)`, `SmootherStep(x, min, max)`, `Average(values)`, `Median(values)`, `CeilTo(value, place?, base?)`, `FloorTo`, `RoundTo`, `RoundAwayFromZero(value)`, `MaxAdd(value, amount, max)`, `MinSub(value, amount, min)`, `Difference(a, b)`, `IsEven(value)`, `IsEvenStrict(value)`, `Factorial(value)`, `Rotate(point, angle)`, `RotateAround(point, cx, cy, angle)`, `RotateAroundDistance(point, cx, cy, angle, dist)`, `TransformXY(x, y, posX, posY, rotation, scaleX, scaleY, output?)`, `GetSpeed(distance, time)`, `RandomXY(vector, scale?)`. + +**Power-of-Two** (`Phaser.Math.Pow2.*`): `GetPowerOfTwo(value)`, `IsValuePowerOfTwo(value)`, `IsSizePowerOfTwo(width, height)`. +</math_functions> + +<vectors_and_matrices> +**Vector2** (`Phaser.Math.Vector2`) — 2D vector for positions, velocities, directions. Constructor: `new Vector2(x?, y?)` or `new Vector2({x, y})`; if only `x` given, `y` defaults to `x`. Methods mutate in place and return `this` for chaining: `set`/`setTo(x, y)`, `setToPolar(angle, length?)`, `copy(src)` / `setFromObject(obj)`, `clone()`, `add`/`subtract(src)`, `multiply`/`divide(src)`, `scale(value)`, `negate()`, `normalize()`, `normalizeRightHand()` / `normalizeLeftHand()` (perpendiculars), `limit(max)`, `setLength(length)`, `length()`, `lengthSq()`, `distance(src)`, `distanceSq(src)`, `dot(src)`, `cross(src)` (2D scalar), `angle()` (radians from +x axis), `setAngle(angle)`, `rotate(delta)`, `lerp(src, t)`, `reflect(normal)`, `mirror(axis)`, `project(src)`, `equals(v)`, `fuzzyEquals(v, epsilon?)`, `ceil()` / `floor()` / `invert()`, `reset()`, `transformMat3(mat)`, `transformMat4(mat)`. Static constants: `Vector2.ZERO`, `RIGHT`, `LEFT`, `UP`, `DOWN`, `ONE`. + +**Vector3** (`Phaser.Math.Vector3`) — for camera projections, lighting, 3D math. Same patterns as Vector2 plus: `up()`, `min(v)`, `max(v)`, `addVectors(a, b)`, `subVectors(a, b)`, `crossVectors(a, b)`, `cross(v)`, `addScalar(s)`, `addScale(v, scale)`, `fromArray(array, offset?)`, `setFromMatrixPosition(mat4)`, `setFromMatrixColumn(mat4, index)`, `applyMatrix3(mat)`, `applyMatrix4(mat)`, `transformCoordinates(mat)`, `transformQuat(q)`, `project(mat)`, `projectViewMatrix(view, proj)`, `unproject(viewport, invProjView)`. + +**Matrix4** (`Phaser.Math.Matrix4`) — 4x4 matrix for 3D transforms, projection, and view matrices, backed by `Float32Array(16)`. Constructor `new Matrix4(m?)` copies an existing Matrix4 or defaults to identity. Key methods: `clone()`, `set`/`copy(src)`, `identity()`, `transpose()`, `invert()`, `adjoint()`, `determinant()`, `multiply`/`multiplyLocal(src)`, `translate(v)`, `scale(v)`, `rotate(angle, axis)`, `rotateX`/`rotateY`/`rotateZ(angle)`, `fromRotationTranslation(q, v)`, `fromQuat(q)`, `frustum(...)`, `perspective(fovy, aspect, near, far)`, `perspectiveLH(width, height, near, far)`, `ortho(left, right, bottom, top, near, far)`, `lookAt(eye, center, up)`, `lookAtRH(eye, target, up)`, `setWorldMatrix(rotation, position, scale, viewMatrix?, projectionMatrix?)`. Also `Phaser.Math.Matrix3` — 3x3 matrix for 2D transforms and normal matrices. +</vectors_and_matrices> + +<random_data_generator> +`Phaser.Math.RandomDataGenerator` — seeded PRNG. Global instance at `Phaser.Math.RND` (seeded via game config `seed`). Provide seeds for reproducible sequences: `new RandomDataGenerator(['level-42'])`. + +```js +const rnd = Phaser.Math.RND; + +rnd.integer(); // random integer in [0, 2^32] +rnd.frac(); // random float in [0, 1) +rnd.between(1, 10); // random integer in [1, 10] +rnd.integerInRange(1, 10); // alias for between +rnd.realInRange(0.5, 1.5); // random float in [0.5, 1.5] +rnd.normal(); // normal distribution around 0 +rnd.angle(); // random angle in radians (-PI to PI) +rnd.rotation(); // random rotation in radians (same range) +rnd.pick(array); // random element +rnd.weightedPick(array); // weighted toward end of array +rnd.sign(); // -1 or 1 +rnd.uuid(); // RFC4122 v4 UUID string +rnd.shuffle(array); // in-place Fisher-Yates shuffle +rnd.state(state?); // get/set deterministic RNG state string +``` +</random_data_generator> + +<color_utilities> +**Color class** (`Phaser.Display.Color`) — mutable RGBA color with automatic conversion to WebGL floats, HSV, CSS strings, and packed integers. + +```js +const color = new Phaser.Display.Color(255, 0, 0, 255); // RGBA (0-255) + +// Creation helpers (return Color instances) +const c1 = Phaser.Display.Color.ValueToColor('#ff0000'); // hex string, integer, or object +const c2 = Phaser.Display.Color.HexStringToColor('#ff0000'); +const c3 = Phaser.Display.Color.RGBStringToColor('rgb(255,0,0)'); +const c4 = Phaser.Display.Color.HSVToRGB(0.5, 1, 1); // returns { r, g, b, color } + +// Properties +color.r; color.g; color.b; color.a; // 0-255 integers +color.redGL; color.greenGL; color.blueGL; color.alphaGL; // 0-1 floats (WebGL) +color.color; // packed 24-bit integer (0xRRGGBB) +color.color32; // packed 32-bit integer (0xAARRGGBB) +color.rgba; // CSS string 'rgba(r,g,b,a)' +color.h; color.s; color.v; // HSV (read-only, auto-updated) +``` + +Manipulation (amount typically 0-100, returns the Color): `brighten(25)`, `saturate(50)`, `desaturate(30)`, `lighten(20)`, `darken(10)`, `random()` (optional min/max range), `gray(128)`. Setting values: `setTo(255, 128, 0, 255)`, `setFromRGB({ r, g, b, a })`, `setFromHSV(0.1, 0.8, 1.0)` (HSV 0-1). + +Interpolation (`Phaser.Display.Color.Interpolate`, returns `{ r, g, b, a, color }`): + +```js +const result = Phaser.Display.Color.Interpolate.RGBWithRGB(255, 0, 0, 0, 0, 255, 100, 50); // midpoint purple +const mid = Phaser.Display.Color.Interpolate.ColorWithColor(color1, color2, 100, 50); +const hsvResult = Phaser.Display.Color.Interpolate.HSVWithHSV(0, 1, 1, 0.5, 1, 1, 100, 50); // v4 smooth hue +``` +</color_utilities> + +<gotchas> +- **Geom objects are not game objects**: no `scene`, no display-list `setPosition`, no texture. Use Graphics or Shape game objects to render them. +- **Point class removed in v4**: use `Phaser.Math.Vector2` or plain `{x, y}` objects. The `GEOM_CONST.POINT` (3) exists but the Point class does not. +- **Angles are in radians** throughout the math API. Use `DegToRad()` / `RadToDeg()` to convert. `Phaser.Math.TAU` (2 * PI) is new in v4. +- **Vector2 methods mutate in place** and return `this`. Call `.clone()` first to preserve the original: `const result = v.clone().add(other)`. +- **Squared distance is faster** than Euclidean (avoids `Math.sqrt`). Use `Distance.Squared` or `vec.distanceSq()` for comparisons where the actual distance value isn't needed. +- **Intersection Get* functions allocate arrays**: pass a reusable `out` array to avoid GC pressure in hot loops. +- **EaseMap short names default to `.Out`**: `'Quad'` means `Quadratic.Out`, not `.In`. Use full keys like `'Quad.easeIn'` for other variants. +- **RandomDataGenerator state is serializable**: `rnd.state()` returns a string; if it must survive reloads, persist that string only as a field in a Base44 entity. +- **Polygon.contains uses ray-casting** (even/odd rule). Complex self-intersecting polygons may give unexpected results. +- **Matrix4.val is a Float32Array**: access elements via `mat.val[index]` in column-major order (OpenGL convention). +</gotchas> +</geometry_and_math> diff --git a/skills/base44-platform-games/references/phaser/graphics_and_shapes.md b/skills/base44-platform-games/references/phaser/graphics_and_shapes.md new file mode 100644 index 0000000..5cc5dba --- /dev/null +++ b/skills/base44-platform-games/references/phaser/graphics_and_shapes.md @@ -0,0 +1,229 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: graphics and shapes + +<graphics_and_shapes> +Use this when drawing shapes and graphics in Phaser 4: the Graphics game object (lines, rectangles, circles, arcs, polygons, gradients, fill, stroke, generated textures) and Shape game objects (Arc, Curve, Ellipse, Grid, IsoBox, IsoTriangle, Line, Polygon, Rectangle, Star, Triangle). + +<quick_start> +```js +// Draw a filled rectangle and stroked circle with Graphics +const gfx = this.add.graphics(); + +gfx.fillStyle(0x00aa00, 1); +gfx.fillRect(50, 50, 200, 100); + +gfx.lineStyle(3, 0xff0000, 1); +gfx.strokeCircle(400, 150, 60); +``` + +```js +// Same shapes as standalone Shape game objects +const rect = this.add.rectangle(150, 100, 200, 100, 0x00aa00); +const circle = this.add.circle(400, 150, 60); +circle.setStrokeStyle(3, 0xff0000); +``` +</quick_start> + +<core_concepts> +Phaser offers two approaches for rendering primitives without textures. + +**Graphics game object** — created with `this.add.graphics(config?)` where config is `{ x?, y?, lineStyle?, fillStyle? }`. An imperative drawing surface: you call methods like `fillRect`, `strokeCircle`, `beginPath`/`lineTo`/`strokePath` to build a command buffer that replays each frame. Supports paths, arcs, gradients, rounded rectangles, canvas transforms (`translateCanvas`, `scaleCanvas`, `rotateCanvas`), and `save`/`restore`. Can bake a Texture via `generateTexture(key, width, height)`. Expensive to render (its own WebGL shader) — group Graphics objects to minimize batch flushes. Components: AlphaSingle, BlendMode, Depth, Lighting, Mask, RenderNodes, Transform, Visible, ScrollFactor. Does NOT include Origin or GetBounds — position via options or `setPosition`. + +**Shape game objects** — individual objects (Arc, Rectangle, Star, etc.) extending the base `Shape` class, each rendering one predefined shape with precomputed path data. Created via dedicated factories (`this.add.rectangle(...)`, `this.add.circle(...)`, etc.). Fully featured: tweenable, scalable, can join groups/containers, enabled for input/physics. Style via `setFillStyle(color, alpha)` and `setStrokeStyle(lineWidth, color, alpha)`. Share the same WebGL batch as Graphics. Do NOT support gradients, path detail threshold, or canvas transforms. Components: AlphaSingle, BlendMode, Depth, GetBounds, Lighting, Mask, Origin, RenderNodes, ScrollFactor, Transform, Visible (include Origin and GetBounds, unlike Graphics). + +**When to use which**: use **Graphics** for dynamic drawing, complex paths, multiple shapes on one object, gradients, or generating textures. Use **Shape objects** for individual UI elements, simple indicators, physics-enabled shapes, or anything that benefits from game object features (origin, bounds, input). +</core_concepts> + +<common_patterns> +Fill and stroke styles (Graphics) — call style methods BEFORE the corresponding fill/stroke; they are not retroactive: + +```js +const gfx = this.add.graphics(); + +gfx.fillStyle(0xff0000, 1); // (color, alpha) — before fill* +gfx.lineStyle(4, 0x00ff00, 1); // (width, color, alpha) — before stroke* + +// Gradient fill (WebGL only) — 4 corner colors +gfx.fillGradientStyle( + 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, // tl, tr, bl, br colors + 1, 1, 1, 1 // tl, tr, bl, br alphas +); +gfx.fillRect(0, 0, 300, 200); + +gfx.lineGradientStyle(2, 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 1); // gradient line (WebGL only) +``` + +Drawing primitives (Graphics): + +```js +gfx.fillRect(x, y, width, height); gfx.strokeRect(x, y, width, height); +gfx.fillCircle(x, y, radius); gfx.strokeCircle(x, y, radius); +gfx.fillEllipse(x, y, width, height, smoothness); // smoothness defaults to 32 +gfx.strokeEllipse(x, y, width, height, smoothness); +gfx.fillTriangle(x0, y0, x1, y1, x2, y2); gfx.strokeTriangle(x0, y0, x1, y1, x2, y2); +gfx.fillPoint(x, y, size); // size defaults to 1 (draws a small square) +gfx.lineBetween(x1, y1, x2, y2); +``` + +Rounded rectangles — radius can be a number or a per-corner object; negative values create concave corners (default 20): + +```js +gfx.fillRoundedRect(50, 50, 300, 200, 16); +gfx.strokeRoundedRect(50, 50, 300, 200, 16); +gfx.fillRoundedRect(50, 50, 300, 200, { tl: 20, tr: 20, bl: 0, br: 0 }); // per-corner +gfx.fillRoundedRect(50, 50, 300, 200, { tl: -10, tr: -10, bl: -10, br: -10 }); // concave +``` + +Path drawing — Graphics `arc`/`slice` take angles in RADIANS: + +```js +gfx.beginPath(); +gfx.moveTo(100, 100); +gfx.lineTo(200, 50); +gfx.lineTo(300, 100); +gfx.lineTo(250, 200); +gfx.closePath(); +gfx.strokePath(); // or gfx.stroke() — alias + +gfx.fillStyle(0x00aaff); +gfx.beginPath(); +gfx.moveTo(100, 100); +gfx.lineTo(200, 50); +gfx.lineTo(300, 100); +gfx.closePath(); +gfx.fillPath(); // or gfx.fill() — alias + +// Arc within a path (x, y, radius, startAngle, endAngle, anticlockwise, overshoot) +gfx.beginPath(); +gfx.arc(200, 200, 80, 0, Math.PI / 2, false, 0); +gfx.strokePath(); + +// Pie slice — auto begins/closes path +gfx.slice(200, 200, 80, 0, Math.PI / 3, false); +gfx.fillPath(); + +// From point arrays +gfx.strokePoints(points, closeShape, closePath, endIndex); +gfx.fillPoints(points, closeShape, closePath, endIndex); +``` + +Geom shape helpers — Graphics accepts `Phaser.Geom` objects directly: `fillCircleShape(circle)`, `strokeCircleShape(circle)`, `fillRectShape(rect)`, `strokeRectShape(rect)`, `fillTriangleShape(tri)`, `strokeTriangleShape(tri)`, `strokeLineShape(line)`, `fillEllipseShape(ellipse, smoothness)`, `strokeEllipseShape(ellipse, smoothness)`. + +Canvas transforms (Graphics): + +```js +gfx.save(); +gfx.translateCanvas(100, 100); +gfx.rotateCanvas(0.5); // radians +gfx.scaleCanvas(2, 2); +gfx.fillStyle(0xff0000); +gfx.fillRect(0, 0, 50, 50); // drawn at translated/rotated/scaled position +gfx.restore(); +``` + +Generating textures from Graphics — gradient fills do NOT appear in generated textures (Canvas API limitation): + +```js +const gfx = this.add.graphics(); +gfx.fillStyle(0xff0000); +gfx.fillCircle(32, 32, 32); +gfx.generateTexture('redCircle', 64, 64); +gfx.destroy(); + +this.add.image(400, 300, 'redCircle'); // now a regular texture +``` + +Shape objects — fill and stroke: + +```js +const rect = this.add.rectangle(200, 150, 100, 80, 0xff0000, 1); + +rect.setFillStyle(0x00ff00, 0.8); // change fill later +rect.setStrokeStyle(3, 0xffffff, 1); // add stroke (not set by default) +rect.setFillStyle(); // no args = isFilled becomes false +rect.setStrokeStyle(); // no args = isStroked becomes false + +// Direct property access +rect.fillColor = 0x0000ff; rect.fillAlpha = 0.5; +rect.strokeColor = 0xffffff; rect.strokeAlpha = 1; rect.lineWidth = 2; +rect.isFilled = true; rect.isStroked = true; +rect.closePath = true; // close stroke path (default true) +``` +</common_patterns> + +<shape_types> +Factory methods (all after `x, y`); defaults in parentheses: +- `this.add.arc(x, y, radius, startAngle, endAngle, anticlockwise, fillColor, fillAlpha)` — class Arc. radius=128, startAngle=0, endAngle=360 (DEGREES), anticlockwise=false. Fill + stroke. +- `this.add.circle(x, y, radius, fillColor, fillAlpha)` — Arc with 0-360 angles. radius=128. Fill + stroke. +- `this.add.curve(x, y, curve, fillColor, fillAlpha)` — Curve, takes a `Phaser.Curves.Curve`. Has `smoothness` / `setSmoothness()`. Fill + stroke. +- `this.add.ellipse(x, y, width, height, fillColor, fillAlpha)` — width=128, height=128 (equal renders as circle). Has `smoothness`. Fill + stroke. +- `this.add.grid(x, y, width, height, cellWidth, cellHeight, fillColor, fillAlpha, outlineFillColor, outlineFillAlpha)` — width/height=128, cellWidth/cellHeight=32. `altFillColor`/`altFillAlpha` for checkerboard; outline via outlineFillColor. Fill only. +- `this.add.isobox(x, y, size, height, fillTop, fillLeft, fillRight)` — size=48, height=32, fillTop=0xeeeeee, fillLeft=0x999999, fillRight=0xcccccc. `showTop`/`showLeft`/`showRight`/`projection`. Fill only. +- `this.add.isotriangle(x, y, size, height, reversed, fillTop, fillLeft, fillRight)` — isometric pyramid. size=48, height=32, reversed=false. Fill only. +- `this.add.line(x, y, x1, y1, x2, y2, strokeColor, strokeAlpha)` — x1=0, y1=0, x2=128, y2=0. STROKE ONLY; constructor takes stroke color (not fill). +- `this.add.polygon(x, y, points, fillColor, fillAlpha)` — points as array of Vec2, `[x,y,...]` pairs, or `[[x,y],...]`. Fill + stroke. +- `this.add.rectangle(x, y, width, height, fillColor, fillAlpha)` — width/height=128, change via `width`/`height`. Fill + stroke. +- `this.add.star(x, y, points, innerRadius, outerRadius, fillColor, fillAlpha)` — points=5, innerRadius=32, outerRadius=64 (4 points = diamond, more = spikier). Fill + stroke. +- `this.add.triangle(x, y, x1, y1, x2, y2, x3, y3, fillColor, fillAlpha)` — x1=0,y1=128, x2=64,y2=0, x3=128,y3=128. Always closed (use Polygon for open shapes). Fill + stroke. +</shape_types> + +<shape_specific_methods> +Star: + +```js +const star = this.add.star(400, 300, 5, 32, 64, 0xffff00); +star.setPoints(8); // change number of points +star.setInnerRadius(20); +star.setOuterRadius(80); +``` + +Line (tapering — `endWidth` is WebGL only; Canvas uses startWidth only): + +```js +const line = this.add.line(400, 300, 0, 0, 200, 100, 0xffffff); +line.setLineWidth(4, 1); // tapers from 4px to 1px +``` + +Grid (alternating colors, stroke, padding): + +```js +const grid = this.add.grid(400, 300, 320, 240, 32, 32, 0x222222); +grid.setAltFillStyle(0x444444, 1); // checkerboard; no args = disable alternating cells +grid.setStrokeStyle(1, 0x666666, 1); // cell outlines (inherited from Shape) +grid.strokeOutside = true; // v4: stroke the outer border +grid.strokeOutsideIncomplete = false; // skip partial cells on right/bottom edges +grid.cellPadding = 1; // gutter between cells is 2x this value (default 0.5) +``` + +IsoBox / IsoTriangle: + +```js +const box = this.add.isobox(200, 200, 48, 32, 0xeeeeee, 0x999999, 0xcccccc); +box.setFaces(true, true, false); // showTop, showLeft, showRight +box.setProjection(4); + +const tri = this.add.isotriangle(400, 200, 48, 32, false, 0xeeeeee, 0x999999, 0xcccccc); +tri.setReversed(true); // flip upside down +tri.setFaces(true, true, true); +tri.setProjection(4); +``` +</shape_specific_methods> + +<gotchas> +- **Graphics arc() uses radians; the Shape arc factory uses degrees.** Graphics `arc` takes start/end angles in radians; `this.add.arc()` takes them in degrees (0-360). Mixing these up is the most common bug. +- **Set style BEFORE drawing.** `fillStyle`/`lineStyle` must be called before the corresponding fill/stroke method — they are not retroactive. +- **Graphics has no Origin or GetBounds.** Use `setPosition(x, y)` and `displayOriginX`/`displayOriginY` instead. +- **Shape isFilled/isStroked defaults.** A shape created with a `fillColor` parameter has `isFilled = true`, but `isStroked` defaults to `false` — call `setStrokeStyle()` explicitly. +- **Line shape is stroke-only.** Its constructor takes `strokeColor`/`strokeAlpha`, not fillColor. +- **IsoBox/IsoTriangle are fill-only.** They cannot be stroked. Grid supports outline strokes via constructor parameters (`outlineFillColor`, `outlineFillAlpha`). +- **generateTexture uses the Canvas API.** Gradient fills (`fillGradientStyle`) will not appear in generated textures — only Canvas-compatible features are captured. +- **Graphics is expensive.** Each frame the command buffer replays and geometry is rebuilt (WebGL decomposes to polygons). For static shapes, `generateTexture` and use the result as a Sprite. Group Graphics objects to minimize batch breaks. +- **pathDetailThreshold (v4 new).** Graphics has a `pathDetailThreshold` property (default -1, uses config `render.pathDetailThreshold`). Path segments below this pixel threshold are combined, improving WebGL performance on complex shapes. Evaluated in screen pixels, so detail emerges when zoomed in. +- **Rounded rect radius can be an object or number.** Pass `{ tl, tr, bl, br }` for per-corner control; negative values create concave corners; default is 20. +- **Shape closePath property** (default `true`) controls whether the stroke path is auto-closed. Set `false` for open stroked shapes. +- **Shape objects do NOT support tint.** Unlike Sprites/Images, they have no `setTint()` or `tint`. Use `setFillStyle(color, alpha)` and `setStrokeStyle(lineWidth, color, alpha)`. +- **Polygon getBounds() is incorrect with negative points.** If any polygon points have negative coordinates, `getBounds()` returns wrong values. Use `Phaser.Geom.Polygon.GetAABB(polygon.geom)` instead and adjust the returned Rectangle position. +</gotchas> +</graphics_and_shapes> diff --git a/skills/base44-platform-games/references/phaser/groups_and_containers.md b/skills/base44-platform-games/references/phaser/groups_and_containers.md new file mode 100644 index 0000000..8b7a042 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/groups_and_containers.md @@ -0,0 +1,221 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: groups and containers + +<groups_and_containers> +Use this when organizing game objects in Phaser 4: Groups for logical collections and object pooling, Containers for visual grouping with inherited transforms, Layers for render-order bucketing, plus batch operations and nested transforms. + +<quick_start> +```js +// In a Scene's create() method: + +// --- Group: logical collection, no transform, great for pooling --- +const enemies = this.add.group(); +enemies.create(100, 200, 'enemy'); // creates Sprite at (100,200) +enemies.create(300, 200, 'enemy'); + +// --- Container: visual parent with inherited transform --- +const hud = this.add.container(10, 10); +const icon = this.add.image(0, 0, 'heart'); +const label = this.add.text(20, 0, 'x3'); +hud.add([icon, label]); // children move/scale/rotate with hud + +// --- Layer: render-ordering bucket, no position/scale --- +const bgLayer = this.add.layer(); +const fgLayer = this.add.layer(); +bgLayer.add(this.add.image(400, 300, 'sky')); +fgLayer.add(this.add.sprite(400, 300, 'player')); +``` +</quick_start> + +<core_concepts> +Three ways to organize objects, each with a different purpose: + +**Group** (`this.add.group()`): a logical collection / pool. NOT on the display list — its children render individually on the Scene. No shared position, rotation, scale, or alpha. Members can belong to many Groups at once. Supports object pooling (`getFirstDead`, `kill`, `killAndHide`). Use for collections of similar objects (enemies, bullets, coins) and physics-group collisions. Children stored in a `children` Set (unordered, no index access). + +**Container** (`this.add.container(x, y)`): a visual parent on the display list that renders its children, which inherit its position, rotation, scale, and alpha. Exclusive by default — adding a child removes it from its previous Container (use `setExclusive(false)` to override). Use for composite UI (health bars, inventory slots), moving/rotating clusters as one unit, and nested transforms. Children stored in a `list` Array. Each child costs matrix math per frame, and deep nesting multiplies that cost — prefer Group or Layer when transforms aren't needed. + +**Layer** (`this.add.layer()`): a lightweight render-order bucket on the display list. No position, rotation, scale, scroll factor, input, or physics. Applies shared alpha/blend/mask. Children render in layer order, then by depth within the layer. Cannot be added to a Container (but Containers can be added to Layers). + +At a glance: if children must move/rotate together, use Container. If you need pooling or multi-membership, use Group. If you only need render ordering and shared effects, use Layer. +</core_concepts> + +<common_patterns> +Creating and populating Groups: +```js +// Empty group, add existing objects +const gems = this.add.group(); +gems.add(existingSprite); +gems.addMultiple([sprite1, sprite2, sprite3]); + +// Group with config -- creates children automatically +const coins = this.add.group({ + classType: Phaser.GameObjects.Sprite, + key: 'coin', + quantity: 10, // overrides frameQuantity + setXY: { x: 50, y: 300, stepX: 60 }, + setScale: { x: 0.5, y: 0.5 } +}); + +// Custom class type with pool limit +const bullets = this.add.group({ + classType: Bullet, // must accept (scene, x, y, key, frame) + maxSize: 30, + defaultKey: 'bullet', + runChildUpdate: true // calls child.update() each frame +}); +``` + +Object pooling with `get` / `getFirstDead` — deactivate objects instead of destroying them, then reuse inactive ones: +```js +const bullets = this.add.group({ + classType: Phaser.GameObjects.Sprite, + defaultKey: 'bullet', + maxSize: 30 +}); + +// Fire a bullet -- get() finds first inactive member or creates one +function fireBullet(x, y) { + const bullet = bullets.get(x, y); + if (bullet) { + bullet.setActive(true); + bullet.setVisible(true); + bullet.body.velocity.y = -300; // if physics enabled + } +} + +// Deactivate when off-screen or on hit +function killBullet(bullet) { + bullets.killAndHide(bullet); // sets active=false, visible=false + // If using physics, also reset the body: bullet.body.stop(); +} + +// Manual queries +const inactive = bullets.getFirst(false); // first where active===false +const active = bullets.getFirstAlive(); // first where active===true +const dead = bullets.getFirstDead(true, x, y); // first inactive, create if null +``` +Pool methods: `get(x, y, key, frame)` (shortcut for `getFirst(false, true, ...)`), `getFirstAlive`, `getFirstDead`, `getLast` (searches back-to-front), `kill` (active=false), `killAndHide` (active=false + visible=false), `countActive`, `getTotalUsed`, `getTotalFree` (remaining capacity), `isFull`. + +Physics Groups extend Group with automatic body assignment: +```js +// Arcade Physics group -- every member gets a dynamic body +const enemies = this.physics.add.group({ + key: 'enemy', quantity: 5, setXY: { x: 100, y: 100, stepX: 80 } +}); + +// Static physics group -- immovable bodies +const platforms = this.physics.add.staticGroup(); +platforms.create(400, 568, 'ground'); + +this.physics.add.collider(player, platforms); +this.physics.add.overlap(bullets, enemies, onHit); +``` + +Containers with nested transforms: +```js +// HUD that follows camera +const hud = this.add.container(10, 10); +hud.setScrollFactor(0); // pinned to camera +const healthBar = this.add.rectangle(0, 0, 200, 20, 0x00ff00); +const healthText = this.add.text(210, -5, '100 HP'); +hud.add([healthBar, healthText]); + +hud.setPosition(50, 50); // move everything at once +hud.setScale(1.5); // scale propagates to children +hud.setRotation(0.1); // rotation propagates to children +hud.setAlpha(0.8); // alpha affects all children + +// Nested containers +const inventory = this.add.container(300, 500); +for (let i = 0; i < 5; i++) { + const slot = this.add.container(i * 55, 0); + slot.add([ + this.add.rectangle(0, 0, 48, 48, 0x333333), + this.add.image(0, 0, `item-${i}`) + ]); + inventory.add(slot); // Container inside Container +} +``` +Key Container methods: `add` / `addAt` (removes child from display list), `remove(child, destroyChild)`, `getAt` / `getIndex`, `getByName` / `getFirst(prop, val)`, `getAll` / `count`, ordering (`sort`, `swap`, `moveTo`, `moveUp`/`moveDown`, `sendToBack`/`bringToTop`), `each` / `iterate` (iterate passes index), `setScrollFactor(x, y, updateChildren)` (pass `true` to apply to children), `getBounds`, `pointToContainer` (world → local), `setExclusive`, `replace`, `setSize(w, h)` (required before `setInteractive`), `length` (read-only count). + +Layers for render ordering: +```js +const bgLayer = this.add.layer(); +const entityLayer = this.add.layer(); +const uiLayer = this.add.layer(); + +bgLayer.add(this.add.image(400, 300, 'sky')); +entityLayer.add(player); +entityLayer.add(enemy); +uiLayer.add(scoreText); + +bgLayer.setDepth(0); // depth of layers themselves +entityLayer.setDepth(1); +uiLayer.setDepth(2); +enemy.setDepth(5); // depth within entityLayer, not the Scene + +// Shared effects to the entire layer +entityLayer.setAlpha(0.5); +entityLayer.setVisible(false); +entityLayer.setBlendMode(Phaser.BlendModes.ADD); +``` + +Bulk creation with `createMultiple` (accepts a config or array of configs): +```js +const coins = this.add.group(); +coins.createMultiple({ + key: 'coin', + quantity: 20, + setXY: { x: 50, y: 100, stepX: 40, stepY: 0 }, + setScale: { x: 0.5, y: 0.5 }, + setRotation: { value: 0, step: 0.1 }, // each rotated 0.1 more than previous + setAlpha: { value: 1 }, + setOrigin: { x: 0.5, y: 0.5 }, + setDepth: { value: 5 }, + gridAlign: { width: 5, height: 4, cellWidth: 48, cellHeight: 48, x: 100, y: 200 } +}); +``` + +Iterating and batch operations on Groups. Many setters support a step argument that distributes incremental values across members: +```js +const enemies = this.add.group(); +const all = enemies.getChildren(); // array snapshot +const active = enemies.getMatching('active', true); + +enemies.setXY(200, 300); +enemies.incX(5); // add 5 to each member's x +enemies.setVisible(false); +enemies.propertyValueSet('tintTopLeft', 0xff0000); +enemies.playAnimation('walk'); + +// Stepping: first member gets base value, each subsequent adds the step +enemies.setX(100, 50); // x = 100, 150, 200... +enemies.setXY(100, 200, 50, 30); // combined X and Y stepping +enemies.angle(0, 15); // 0, 15, 30 deg... +enemies.setAlpha(1, -0.1); // 1.0, 0.9, 0.8... +enemies.setScale(1, 0, 0.1, 0); // scaleX: 1.0, 1.1, 1.2... +enemies.setDepth(0, 1); // depth 0, 1, 2, 3... +enemies.setTint(0xff0000); +enemies.shuffle(); // randomize order +``` +</common_patterns> + +<gotchas> +- Group is NOT on the display list. Its children appear on the Scene individually. Moving a Group does nothing visually — use a Container for that. +- Container has performance overhead — every child requires extra matrix math per frame, multiplied by nesting depth. Avoid Containers when a Group or Layer suffices. +- Container origin is always 0,0 and cannot be changed. Position children relative to (0,0). +- Container children lose Scene-level depth control. A child's `depth` only orders within the Container; the Container's own depth positions it in the Scene. +- Physics + Container is problematic. If a Container is not at (0,0), physics bodies on children are offset. Avoid physics bodies on Container children. +- Container children cannot be individually masked in Canvas rendering — only the Container itself can have a mask, and masks don't stack for nested Containers. Masks DO stack in WebGL. +- `Group.get(x, y)` is shorthand for `getFirst(false, true, x, y)` — finds first inactive member and creates if none. `getFirst(state)` defaults to `active===false` without auto-creating. +- Layer cannot go inside a Container (only the reverse). +- Group children Set is unordered — no index access; use `getChildren()` for an array snapshot. +- Phaser 4 removed `children.each` / `children.iterate` from Group — Container still has them, Group does not. `children.forEach` replaces them, but `return false` no longer breaks out of it. +- `killAndHide` does NOT remove from the group — it only sets `active=false` and `visible=false`. The object stays for reuse. +- `Container.setScrollFactor` does not auto-propagate. Pass `true` as the third argument to also update children: `container.setScrollFactor(0, 0, true)`. +- `Group.create()` adds to the Scene display list, but `group.add()` does NOT unless you pass `true` as the second argument. +- Container needs `setSize(width, height)` before `setInteractive()` will work with a hit area — Containers have no implicit size. +</gotchas> +</groups_and_containers> diff --git a/skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md b/skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md new file mode 100644 index 0000000..a425d9c --- /dev/null +++ b/skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md @@ -0,0 +1,310 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: input keyboard mouse touch + +<input_keyboard_mouse_touch> +Use this when handling user input in Phaser 4: keyboard keys, mouse clicks and movement, touch events, pointer handling, drag and drop, hit areas, interactive objects, and gamepad support. Input can be handled through event listeners or by polling state each frame. + +<quick_start> +```js +class MyScene extends Phaser.Scene { + create() { + // Keyboard: create cursor keys (up, down, left, right, space, shift) + this.cursors = this.input.keyboard.createCursorKeys(); + + // Keyboard: listen for a specific key event + this.input.keyboard.on('keydown-SPACE', (event) => { + console.log('Space pressed'); + }); + + // Pointer: listen for click/tap anywhere on the game canvas + this.input.on('pointerdown', (pointer) => { + console.log('Clicked at', pointer.x, pointer.y); + }); + + // Pointer: make a Game Object interactive and clickable + const sprite = this.add.sprite(400, 300, 'player'); + sprite.setInteractive(); + sprite.on('pointerdown', (pointer, localX, localY, event) => { + console.log('Sprite clicked at local', localX, localY); + }); + } + + update() { + // Poll cursor keys each frame + if (this.cursors.left.isDown) { /* move left */ } + if (this.cursors.right.isDown) { /* move right */ } + if (Phaser.Input.Keyboard.JustDown(this.cursors.space)) { + // fire once per press + } + } +} +``` +</quick_start> + +<core_concepts> +The input plugin (`this.input`) is an `EventEmitter` accessed in any Scene that handles all input for that Scene. Key properties: `enabled` (toggle input processing), `topOnly` (default `true` — only emit events from the top-most object under the pointer), `keyboard` (KeyboardPlugin), `gamepad` (GamepadPlugin), `mouse` (MouseManager), `activePointer` (most recently active Pointer), `mousePointer` (pointers[0], distinct from pointer1 = first touch), `pointer1`–`pointer10`, `dragDistanceThreshold` (px before drag starts), `dragTimeThreshold` (ms before drag starts), `pollRate` (-1 = poll only on movement, 0 = every frame). Key methods: `addPointer(quantity)` for multi-touch (default 2, max 10), `setHitArea*` family (`setHitAreaCircle`, `setHitAreaEllipse`, `setHitAreaRectangle`, `setHitAreaTriangle`, `setHitAreaFromTexture`), `setDraggable(gameObjects, value)`, `makePixelPerfect(alphaTolerance)`. + +A `Pointer` encapsulates both mouse and touch. Key properties: `x`/`y` (screen space), `worldX`/`worldY` (through the most recent Camera), `downX`/`downY`, `upX`/`upY`, `isDown` (any button held), `primaryDown` (primary button held), `button` (0=left, 1=middle, 2=right), `buttons` (bitmask: 1=left, 2=right, 4=middle, 8=back, 16=forward), `wasTouch`, `velocity` (Vector2), `angle`, `distance`, `movementX`/`movementY` (when locked), `deltaX`/`deltaY`/`deltaZ` (wheel), `locked`, `camera`. Methods: `leftButtonDown()` / `rightButtonDown()` / `middleButtonDown()` / `backButtonDown()` / `forwardButtonDown()`, the `*Released()` variants, `getDistance()` / `getDistanceX()` / `getDistanceY()`, `getDuration()`, `getAngle()`, `updateWorldPoint(camera)`, `positionToCamera(camera, output)`. + +Call `gameObject.setInteractive()` to enable input — it uses the texture frame as the hit area by default: +```js +sprite.setInteractive(); // default hit area from texture + +// Custom shape hit areas +sprite.setInteractive(new Phaser.Geom.Circle(32, 32, 32), Phaser.Geom.Circle.Contains); +sprite.setInteractive(new Phaser.Geom.Ellipse(50, 50, 100, 60), Phaser.Geom.Ellipse.Contains); +sprite.setInteractive(new Phaser.Geom.Triangle(0,64,32,0,64,64), Phaser.Geom.Triangle.Contains); +sprite.setInteractive(new Phaser.Geom.Polygon(points), Phaser.Geom.Polygon.Contains); + +// Pixel-perfect hit testing (expensive — use sparingly) +sprite.setInteractive({ pixelPerfect: true, alphaTolerance: 1 }); +sprite.setInteractive(this.input.makePixelPerfect()); +sprite.setInteractive(this.input.makePixelPerfect(150)); // with alpha tolerance + +// Config object with multiple options +sprite.setInteractive({ + draggable: true, dropZone: false, useHandCursor: true, + cursor: 'pointer', pixelPerfect: true, alphaTolerance: 1 +}); + +// Containers must specify a shape or call setSize first +container.setSize(200, 200); +container.setInteractive(); +``` +</core_concepts> + +<keyboard> +Cursor keys return an object with `up`, `down`, `left`, `right`, `space`, `shift` Key objects: +```js +this.cursors = this.input.keyboard.createCursorKeys(); +// In update(): +if (this.cursors.up.isDown) { /* held */ } +if (this.cursors.space.isDown) { /* held */ } +``` + +`addKey` creates a single Key; `addKeys` creates several at once: +```js +const keyW = this.input.keyboard.addKey('W'); // by name +const keySpace = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); // by code +const keyA = this.input.keyboard.addKey('A', true, false); // addKey(key, enableCapture, emitOnRepeat) + +const keys = this.input.keyboard.addKeys('W,S,A,D'); // returns { W, S, A, D } +if (keys.W.isDown) { /* ... */ } + +const named = this.input.keyboard.addKeys({ // object form, custom names + up: Phaser.Input.Keyboard.KeyCodes.W, + down: Phaser.Input.Keyboard.KeyCodes.S, + left: Phaser.Input.Keyboard.KeyCodes.A, + right: Phaser.Input.Keyboard.KeyCodes.D +}); +``` + +Polling vs events: +```js +// Polling in update() +if (keyW.isDown) { /* currently held */ } +if (keyW.isUp) { /* currently released */ } + +// JustDown / JustUp — true only once per press, resets after check +if (Phaser.Input.Keyboard.JustDown(keyW)) { /* fire once */ } +if (Phaser.Input.Keyboard.JustUp(keyW)) { /* released once */ } + +// checkDown with duration — throttled polling +if (this.input.keyboard.checkDown(keySpace, 250)) { /* at most once every 250ms while held */ } + +// Event-driven +this.input.keyboard.on('keydown-SPACE', (event) => { /* ... */ }); +this.input.keyboard.on('keyup-SPACE', (event) => { /* ... */ }); +this.input.keyboard.on('keydown', (event) => { console.log(event.key); }); // any key, native DOM event + +// Event on a Key object itself +const spaceBar = this.input.keyboard.addKey('SPACE'); +spaceBar.on('down', (key, event) => { /* Key object + native event */ }); +spaceBar.on('up', (key, event) => { /* ... */ }); +``` + +Key object properties: `isDown`/`isUp`, `keyCode`, modifier state `altKey`/`ctrlKey`/`shiftKey`/`metaKey`, `duration` (ms held in previous cycle), `timeDown`/`timeUp`, `repeats`, `emitOnRepeat` (fires 'down' on each repeat), `enabled`. + +Prevent browser defaults (captures are global across all Scenes): +```js +this.input.keyboard.addCapture('SPACE,UP,DOWN,LEFT,RIGHT'); +this.input.keyboard.addCapture([ 32, 37, 38, 39, 40 ]); +this.input.keyboard.removeCapture('SPACE'); +``` + +Common KeyCodes (access via `Phaser.Input.Keyboard.KeyCodes.SPACE` etc.): `BACKSPACE(8)`, `TAB(9)`, `ENTER(13)`, `SHIFT(16)`, `CTRL(17)`, `ALT(18)`, `ESC(27)`, `SPACE(32)`, `LEFT(37)`, `UP(38)`, `RIGHT(39)`, `DOWN(40)`, `A-Z(65-90)`, `ZERO-NINE(48-57)`, `F1-F12(112-123)`. + +Key combos listen for a sequence of keys: +```js +this.input.keyboard.createCombo('PHASER'); // string-based +this.input.keyboard.createCombo( // array of codes (Konami) + [ 38, 38, 40, 40, 37, 39, 37, 39, 66, 65, 13 ], + { resetOnMatch: true } +); +this.input.keyboard.on('keycombomatch', (keyCombo, event) => { console.log('Combo matched!'); }); +``` +`createCombo(keys, config)` — `keys` is a string (each char a key) or array of codes/Key objects. Config: `resetOnWrongKey` (default true), `maxKeyDelay` (max ms between presses, 0 = no limit), `resetOnMatch` (default false), `deleteOnMatch` (default false). +</keyboard> + +<mouse_and_pointer> +Scene-level pointer events fire anywhere on the canvas: +```js +this.input.on('pointerdown', (pointer, currentlyOver) => { + // currentlyOver: array of interactive Game Objects under the pointer +}); +this.input.on('pointerup', (pointer, currentlyOver) => { /* ... */ }); +this.input.on('pointermove', (pointer, currentlyOver) => { /* ... */ }); +this.input.on('wheel', (pointer, currentlyOver, deltaX, deltaY, deltaZ) => { /* ... */ }); +``` + +Game Object pointer events require `setInteractive`: +```js +sprite.setInteractive(); +sprite.on('pointerdown', (pointer, localX, localY, event) => { + // localX/localY relative to the object's top-left; event.stopPropagation() halts further dispatch +}); +sprite.on('pointerup', (pointer, localX, localY, event) => { /* ... */ }); +sprite.on('pointermove', (pointer, localX, localY, event) => { /* ... */ }); +sprite.on('pointerover', (pointer, localX, localY, event) => { /* ... */ }); +sprite.on('pointerout', (pointer, event) => { /* ... */ }); +sprite.on('wheel', (pointer, deltaX, deltaY, deltaZ, event) => { /* ... */ }); +``` + +Pointer events fire at three levels: (1) Game Object — `gameObject.on('pointerdown', (pointer, localX, localY, event) => {})`; (2) Scene per-object — `this.input.on('gameobjectdown', (pointer, gameObject, event) => {})`; (3) Scene global — `this.input.on('pointerdown', (pointer, currentlyOver) => {})`. + +Right-click and context menu: +```js +this.input.on('pointerdown', (pointer) => { + if (pointer.rightButtonDown()) { /* right-click */ } +}); +this.input.mouse.disableContextMenu(); +``` + +Pointer lock (FPS-style mouse capture): +```js +this.input.on('pointerdown', () => { this.input.mouse.requestPointerLock(); }); +this.input.on('pointerlockchange', (event, locked) => { /* locked: boolean */ }); +// Read relative movement while locked: pointer.movementX, pointer.movementY +``` + +Debug, topOnly, and multi-touch: +```js +this.input.enableDebug(gameObject); // visualize hit areas +this.input.enableDebug(gameObject, 0xff00ff); +this.input.removeDebug(gameObject); + +this.input.topOnly = false; // let all objects under pointer receive events +this.input.setTopOnly(false); +``` +Multi-touch: set `config.input.activePointers` to reserve slots at startup, or call `this.input.addPointer(num)` at runtime. Access via `this.input.pointer1` through `pointer10`. +</mouse_and_pointer> + +<drag_and_drop> +```js +const sprite = this.add.sprite(400, 300, 'item'); +sprite.setInteractive(); +this.input.setDraggable(sprite); // or sprite.setInteractive({ draggable: true }); + +// Scene-level drag events +this.input.on('dragstart', (pointer, gameObject) => { gameObject.setTint(0xff0000); }); +this.input.on('drag', (pointer, gameObject, dragX, dragY) => { + gameObject.x = dragX; + gameObject.y = dragY; +}); +this.input.on('dragend', (pointer, gameObject) => { gameObject.clearTint(); }); + +// Drop zones +const zone = this.add.zone(600, 300, 200, 200).setRectangleDropZone(200, 200); +this.input.on('drop', (pointer, gameObject, dropZone) => { + gameObject.x = dropZone.x; + gameObject.y = dropZone.y; +}); +this.input.on('dragenter', (pointer, gameObject, dropZone) => { /* ... */ }); +this.input.on('dragleave', (pointer, gameObject, dropZone) => { /* ... */ }); +this.input.on('dragover', (pointer, gameObject, dropZone) => { /* ... */ }); +``` + +Game Object-level drag events are also available: +```js +sprite.on('drag', (pointer, dragX, dragY) => { sprite.x = dragX; sprite.y = dragY; }); +sprite.on('dragstart', (pointer, dragX, dragY) => { /* ... */ }); +sprite.on('dragend', (pointer, dragX, dragY) => { /* ... */ }); +sprite.on('drop', (pointer, dropZone) => { /* ... */ }); +``` + +Drag thresholds: +```js +this.input.dragDistanceThreshold = 16; // must move 16px before drag starts +this.input.dragTimeThreshold = 200; // must hold 200ms before drag starts +``` +</drag_and_drop> + +<gamepad> +Enable gamepads in the game config, then access via `this.input.gamepad`. Pads are `pad1` through `pad4`: +```js +const config = { input: { gamepad: true } }; + +this.input.gamepad.once('connected', (pad) => { console.log('Gamepad connected:', pad.id); }); +if (this.input.gamepad.total > 0) { + const pad = this.input.gamepad.pad1; +} +``` + +Polling gamepad state in update(): +```js +update() { + const pad = this.input.gamepad.pad1; + if (!pad) return; + + if (pad.up) { /* d-pad up */ } // D-pad (boolean) + if (pad.down) { /* ... */ } + if (pad.left) { /* ... */ } + if (pad.right) { /* ... */ } + + if (pad.A) { /* bottom button (Xbox A / PS X) */ } // face buttons (boolean), Xbox naming + if (pad.B) { /* right (Xbox B / PS Circle) */ } + if (pad.X) { /* left (Xbox X / PS Square) */ } + if (pad.Y) { /* top (Xbox Y / PS Triangle) */ } + + if (pad.L1 > 0) { /* left shoulder (LB) */ } // shoulders (float 0-1) + if (pad.L2 > 0) { /* left trigger (LT) */ } + if (pad.R1 > 0) { /* right shoulder (RB) */ } + if (pad.R2 > 0) { /* right trigger (RT) */ } + + const lx = pad.leftStick.x; // analog sticks (Vector2, -1 to 1) + const ly = pad.leftStick.y; + const rx = pad.rightStick.x; + const ry = pad.rightStick.y; + + pad.getAxisValue(0); // raw axis (float) + pad.getButtonValue(0); // raw button (float 0-1) + pad.isButtonDown(0); // boolean + pad.setAxisThreshold(0.1); // ignore values below threshold +} +``` + +Gamepad events: +```js +// Plugin-level (any gamepad) +this.input.gamepad.on('connected', (pad, event) => { /* ... */ }); +this.input.gamepad.on('disconnected', (pad, event) => { /* ... */ }); +this.input.gamepad.on('down', (pad, button, value) => { /* any button on any pad */ }); +this.input.gamepad.on('up', (pad, button, value) => { /* ... */ }); + +// Gamepad-instance +pad.on('down', (index, value, button) => { /* button on this specific pad */ }); +pad.on('up', (index, value, button) => { /* ... */ }); +``` + +Vibration (experimental, hardware/browser dependent): +```js +if (pad.vibration) { + pad.vibration.playEffect('dual-rumble', { + duration: 200, strongMagnitude: 1.0, weakMagnitude: 0.5 + }); +} +``` +</gamepad> +</input_keyboard_mouse_touch> diff --git a/skills/base44-platform-games/references/phaser/loading_assets.md b/skills/base44-platform-games/references/phaser/loading_assets.md new file mode 100644 index 0000000..fe867c6 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/loading_assets.md @@ -0,0 +1,265 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: loading assets + +<loading_assets> +Use this when loading assets in Phaser 4: the Loader plugin (`this.load`) for images, spritesheets, atlases, audio, JSON, tilemaps, bitmap fonts, and more. Assets are queued in `preload()`, loaded in parallel, and placed into global caches accessible by every Scene. + +<quick_start> +```js +class GameScene extends Phaser.Scene { + preload() { + this.load.image('logo', 'assets/logo.png'); + } + create() { + this.add.image(400, 300, 'logo'); + } +} +``` +Assets loaded in `preload()` are guaranteed ready when `create()` runs. The Loader starts automatically during the preload phase. +</quick_start> + +<core_concepts> +Every Scene can define a `preload()` method. Queued files don't load immediately — the Loader starts when `preload()` completes and waits for all files before calling `create()`: +```js +preload() { + this.load.image('sky', 'assets/sky.png'); + this.load.spritesheet('dude', 'assets/dude.png', { frameWidth: 32, frameHeight: 48 }); + this.load.audio('jump', 'assets/jump.mp3'); +} +create() { + this.add.image(400, 300, 'sky'); + this.add.sprite(100, 450, 'dude'); + this.sound.play('jump'); +} +``` + +Loading outside `preload()` (e.g. in `create()` or in response to a user action) requires manually starting the Loader: +```js +create() { + this.load.image('extra', 'assets/extra.png'); + this.load.once('complete', () => { this.add.image(400, 300, 'extra'); }); + this.load.start(); +} +``` + +URL resolution is `baseURL + path + filename`, set via game config or at runtime. The prefix is prepended to the cache key, NOT the URL. Absolute URLs bypass path/baseURL: +```js +preload() { + this.load.setBaseURL('https://cdn.example.com/'); + this.load.setPath('assets/images/'); + this.load.setPrefix('LEVEL1.'); + // Loads from https://cdn.example.com/assets/images/hero.png, cached as 'LEVEL1.hero' + this.load.image('hero', 'hero.png'); + this.load.image('cloud', 'https://other-server.com/cloud.png'); // absolute, bypasses path +} +``` +These can also be set in the game config under `loader: { baseURL, path, prefix, maxParallelDownloads, crossOrigin, responseType, async, timeout, maxRetries, imageLoadType }`. + +Assets go into global game-level caches, not per-Scene — an image loaded in one Scene is available everywhere. Textures go into `this.textures` (the Texture Manager); other data goes into `this.cache` sub-caches (`this.cache.json`, `this.cache.audio`, `this.cache.xml`, etc.). + +The Loader emits events throughout the lifecycle — use these for progress bars and loading screens: +```js +preload() { + this.load.on('progress', (value) => { /* value 0..1 */ }); + this.load.on('complete', () => { console.log('All assets loaded'); }); + this.load.on('loaderror', (file) => { console.warn('Failed to load:', file.key); }); + this.load.image('bg', 'assets/bg.png'); +} +``` +</core_concepts> + +<common_patterns> +Images and sprite sheets: +```js +preload() { + this.load.image('star', 'assets/star.png'); + this.load.image('brick', ['assets/brick.png', 'assets/brick_n.png']); // [texture, normalMap] + this.load.spritesheet('explosion', 'assets/explosion.png', { + frameWidth: 64, frameHeight: 64, startFrame: 0, endFrame: 23, margin: 0, spacing: 0 + }); + this.load.svg('logo', 'assets/logo.svg', { width: 400, height: 400 }); // optional rasterize size +} +``` + +Audio — always provide multiple formats for cross-browser support: +```js +preload() { + this.load.audio('bgm', 'assets/music.mp3'); + this.load.audio('bgm', ['assets/music.ogg', 'assets/music.mp3']); // first supported wins + this.load.audioSprite('sfx', 'assets/sfx.json', ['assets/sfx.ogg', 'assets/sfx.mp3']); +} +``` + +JSON and tilemaps: +```js +preload() { + this.load.json('levelData', 'assets/level1.json'); + this.load.json('enemies', 'assets/data.json', 'enemies'); // dataKey extracts a sub-object + this.load.tilemapTiledJSON('map', 'assets/map.json'); // Tiled JSON + this.load.tilemapCSV('csvmap', 'assets/level.csv'); + this.load.tilemapImpact('impactmap', 'assets/level.js'); +} +create() { + const data = this.cache.json.get('levelData'); + const map = this.make.tilemap({ key: 'map' }); +} +``` + +Atlases: +```js +preload() { + this.load.atlas('sprites', 'assets/sprites.png', 'assets/sprites.json'); // TexturePacker JSON + this.load.atlasXML('ui', 'assets/ui.png', 'assets/ui.xml'); // Starling/Sparrow + this.load.multiatlas('world', 'assets/world.json', 'assets/'); // split across textures + this.load.unityAtlas('chars', 'assets/chars.png', 'assets/chars.txt'); + this.load.aseprite('knight', 'assets/knight.png', 'assets/knight.json'); +} +create() { + this.add.sprite(400, 300, 'sprites', 'walk_01'); +} +``` + +Bitmap fonts (need both a texture and an XML/JSON data file), video, and web fonts: +```js +preload() { + this.load.bitmapFont('pixels', 'assets/font.png', 'assets/font.xml'); + this.load.video('intro', 'assets/intro.mp4'); + this.load.video('bg_loop', 'assets/loop.mp4', true); // third arg = noAudio + this.load.font('myFont', 'assets/myfont.ttf', 'truetype'); + this.load.font('boldFont', 'assets/bold.woff2', 'woff2', { weight: 'bold', style: 'normal' }); +} +create() { + this.add.bitmapText(100, 100, 'pixels', 'Hello World', 32); +} +``` + +Pack files — a JSON manifest describing multiple assets to load at once: +```js +preload() { this.load.pack('pack1', 'assets/pack.json'); } +``` +```json +{ + "section1": { + "baseURL": "assets/", + "files": [ + { "type": "image", "key": "bg", "url": "bg.png" }, + { "type": "atlas", "key": "chars", "textureURL": "chars.png", "atlasURL": "chars.json" } + ] + } +} +``` +Pack data can also be inline instead of from a URL: +```js +preload() { + this.load.pack('manifest', { + section1: { + prefix: '', path: 'assets/', defaultType: 'image', + files: [ + { type: 'image', key: 'bg', url: 'bg.png' }, + { type: 'spritesheet', key: 'player', url: 'player.png', frameConfig: { frameWidth: 32, frameHeight: 32 } } + ] + } + }); +} +``` + +Progress bar: +```js +preload() { + const progressBar = this.add.graphics(); + const progressBox = this.add.graphics(); + progressBox.fillStyle(0x222222, 0.8); + progressBox.fillRect(240, 270, 320, 50); + + this.load.on('progress', (value) => { + progressBar.clear(); + progressBar.fillStyle(0xffffff, 1); + progressBar.fillRect(250, 280, 300 * value, 30); + }); + this.load.on('complete', () => { progressBar.destroy(); progressBox.destroy(); }); + + this.load.image('sky', 'assets/sky.png'); + // ... more assets +} +``` + +All `this.load` methods accept positional arguments or a single config object, and an array of config objects to batch-load. Available file types: textures (`image`, `spritesheet`, `atlas`, `atlasXML`, `multiatlas`, `unityAtlas`, `aseprite`, `svg`, `htmlTexture`, `texture`), audio/video (`audio`, `audioSprite`, `video`), data (`json`, `xml`, `text`, `binary`, `html`, `css`, `glsl`), fonts (`bitmapFont`, `font`), tilemaps (`tilemapTiledJSON`, `tilemapCSV`, `tilemapImpact`), and `animation`, `pack`, `script`, `scripts`, `plugin`, `scenePlugin`, `sceneFile`. +</common_patterns> + +<events> +All events are emitted on the Loader instance (`this.load`): +- `'addfile'` `(key, type, loader, file)` — a file was added to the queue +- `'start'` `(loader)` — Loader started, progress is zero +- `'load'` `(file)` — a single file finished downloading (before processing/caching) +- `'fileprogress'` `(file, percentComplete)` — per-file download progress (0-1); only fires if the browser provides `lengthComputable` +- `'progress'` `(value)` — overall load progress (0-1) +- `'postprocess'` `(loader)` — all files loaded and processed, before internal cleanup +- `'filecomplete'` `(key, type, data)` — any file finished loading and processing +- `'filecomplete-{type}-{key}'` `(key, type, data)` — a specific file finished (e.g. `'filecomplete-image-hero'`) +- `'loaderror'` `(file)` — a file failed to load +- `'complete'` `(loader, totalComplete, totalFailed)` — all files done + +Lifecycle order: `start` → `fileprogress` (repeats per file) → `load` → `filecomplete-{type}-{key}` → `filecomplete` → `progress` → `postprocess` → `complete`. +</events> + +<advanced> +Scene payload — load files before `preload()` runs by defining a `pack` in the Scene constructor (useful for progress-bar assets): +```js +class BootScene extends Phaser.Scene { + constructor() { + super({ + key: 'BootScene', + pack: { files: [ { type: 'image', key: 'bar', url: 'loaderBar.png' } ] } + }); + } + // 'bar' is already available in preload() +} +``` + +Adding files mid-load — chain dependent loads via the per-file completion event: +```js +preload() { + this.load.json('level1', 'level1.json'); + this.load.on('filecomplete-json-level1', (key, type, data) => { + this.load.image(data.images); + this.load.spritesheet(data.spritesheets); + }); +} +``` + +Cache system — loaded assets go into global caches shared across Scenes. Textures live in `this.textures`; other types in typed sub-caches under `this.cache` (`text`, `json`, `audio`, `binary`, `shader`, `xml`): +```js +const json = this.cache.json.get('levelData'); +const text = this.cache.text.get('dialogue'); + +this.cache.json.exists('levelData'); // or .has('levelData') +this.textures.exists('hero'); + +this.textures.remove('hero'); // free memory +this.cache.audio.remove('bgm'); + +this.textures.on('addtexture-mapKey', (texture) => { + // Another scene loaded 'mapKey' — now available here +}); +``` +</advanced> + +<gotchas> +- Keys must be unique within their type. A second `this.load.image('bg', ...)` is silently ignored if `'bg'` already exists. Remove the old texture (`this.textures.remove`) first to replace it. +- A sprite sheet is not an atlas. Use `spritesheet()` for fixed-size grids referenced by index; use `atlas()` for packed textures with named frames. +- Outside `preload()` the Loader does NOT auto-start — call `this.load.start()` manually. +- Path must end with `/`. `this.load.setPath()` appends the slash automatically; setting `this.load.path` directly requires the trailing slash yourself. +- Always provide multiple audio formats (OGG + MP3 minimum) — the Loader picks the first the browser supports. +- Pack file sections can each override `baseURL`/`path`/`prefix`; those apply only within the section and are restored after. +- The cache key includes the prefix. With `setPrefix('MENU.')`, an image keyed `'bg'` is cached as `'MENU.bg'` — reference it by the full key. +- `maxRetries` (default 2) is captured per-file at creation time; changing it afterward has no effect on already-queued files. +- `imageLoadType: 'HTMLImageElement'` in the loader config uses `<img>` tag loading instead of XHR blobs, which can help with CORS in some environments. +- `file://` and `capacitor://` are treated as local schemes (via `localSchemes`) and skip CORS headers. +- Set `crossOrigin: 'anonymous'` (or `this.load.setCORS('anonymous')`) when loading from another domain, especially for WebGL textures. +- Keys are case-sensitive and scoped per type — `'Player'` ≠ `'player'`; an image and audio asset can share key `'player'`. +- Scene `update()` does NOT fire during preload (it's paused), but `preupdate`, `postupdate`, and `render` still fire. +- Progress can decrease — if files are added mid-load (e.g. via `filecomplete` chaining), the value drops because total count rose. +</gotchas> +</loading_assets> diff --git a/skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md b/skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md new file mode 100644 index 0000000..e17c558 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md @@ -0,0 +1,50 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: mesh and 3d elements + +<mesh_and_3d_elements> +Use this when a 2D Phaser game wants a 3D LOOK or depth flourishes without leaving 2D — Phaser stays the only engine (physics, input, scenes, collision are all unchanged). These are pseudo-3D tricks: plain 2D, no new machinery, not real 3D. + +Note: Phaser's `Mesh`/`Plane` game objects from v3 (`this.add.mesh()`, `Phaser.Geom.Mesh.GenerateGridVerts`, `panZ`, `modelRotation`) were REMOVED in Phaser 4 — there is no Phaser-native vehicle for unlit 3D accents anymore. Small flourishes that used to lean on Mesh (card flips, tilting panels) are done with pseudo-3D scale/angle tweens, frame swaps, or a sheared Graphics quad. + +<tier1_pseudo_3d> +Fake depth with plain 2D: +```js +// Depth scaling: things lower on screen are "closer" +sprite.setScale(0.5 + (sprite.y / this.scale.height) * 0.7); +sprite.setDepth(sprite.y); + +// Parallax background layers (farther = slower) +this.add.tileSprite(0, 0, w, h, 'far-hills').setOrigin(0).setScrollFactor(0.2); +this.add.tileSprite(0, 0, w, h, 'near-trees').setOrigin(0).setScrollFactor(0.6); + +// Isometric placement + draw-order sorting +const screenX = (tileX - tileY) * TILE_W / 2; +const screenY = (tileX + tileY) * TILE_H / 2; +obj.setDepth(screenY); +``` +Floor-stripe "mode 7" feel: scale + tint rows of tileSprites by distance instead of attempting real projection. + +Fake a 3D card flip without any Mesh — tween `scaleX` through 0 and swap the texture at the midpoint: +```js +this.tweens.add({ + targets: card, + scaleX: 0, + duration: 250, + ease: 'Cubic.easeIn', + onComplete: () => { + card.setTexture('card-front'); + this.tweens.add({ targets: card, scaleX: 1, duration: 250, ease: 'Cubic.easeOut' }); + }, +}); +``` +Tilting panels read well as a short `angle` + `scaleY` tween, a frame swap between pre-drawn angles, or a sheared quad drawn with Graphics (`fillPoints`). (Phaser game objects have no skew transform — a `skewX` tween renders nothing.) +</tier1_pseudo_3d> + +<common_mistakes> +- Reaching for `this.add.mesh()` / `Phaser.Geom.Mesh` / `panZ` / `modelRotation` — the Phaser Mesh and Plane objects were removed in v4. Use a pseudo-3D tween, frame swap, or sheared Graphics quad instead. +- Expecting a `skewX` tween to do anything — Phaser game objects have no skew transform; shear with a Graphics `fillPoints` quad instead. +- Forgetting depth sorting (`setDepth`) on overlapping pseudo-3D sprites — closer objects then draw in front of farther ones. +</common_mistakes> +</mesh_and_3d_elements> diff --git a/skills/base44-platform-games/references/phaser/particles.md b/skills/base44-platform-games/references/phaser/particles.md new file mode 100644 index 0000000..0a063c0 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/particles.md @@ -0,0 +1,290 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: particles + +<particles> +Use this when creating particle effects in Phaser 4: ParticleEmitter creation and configuration, emitter op value formats, gravity wells, emission and death zones, flow vs burst modes, following game objects, and particle callbacks. + +<quick_start> +```js +// In a Scene's create() method: + +// Basic continuous emitter (flow mode) +const emitter = this.add.particles(400, 300, 'flares', { + frame: 'red', + speed: 200, + lifespan: 2000, + scale: { start: 1, end: 0 }, + alpha: { start: 1, end: 0 }, + gravityY: 150 +}); + +// One-shot burst (explode mode) +const burst = this.add.particles(400, 300, 'flares', { + frame: 'blue', + speed: { min: 100, max: 300 }, + lifespan: 1000, + scale: { start: 0.5, end: 0 }, + emitting: false // don't auto-start +}); +burst.explode(20); // emit 20 particles at once +``` +</quick_start> + +<core_concepts> +`ParticleEmitter` extends `GameObject` and is added directly to the display list — it is both a positionable/scalable/maskable game object and the emitter itself. There is no separate manager; `this.add.particles()` returns a `ParticleEmitter`. +```js +this.add.particles(x, y, texture, config); +// x, y: world position (both optional, default 0) +// texture: string key or Texture instance +// config: ParticleEmitterConfig object (optional, can call setConfig later) +``` +Mixins (AlphaSingle, BlendMode, Depth, Lighting, Mask, RenderNodes, ScrollFactor, Texture, Transform, Visible) mean you can call `setPosition()`, `setScale()`, `setDepth()`, `setBlendMode()`, `setMask()`, `setScrollFactor()`, etc. + +A `Particle` is a lightweight, internally-pooled object owned by its emitter — you never create them manually. Key properties: `x`, `y`, `velocityX/Y`, `accelerationX/Y`, `scaleX/Y`, `alpha`, `angle`, `rotation`, `tint`, `life` (total ms), `lifeCurrent` (remaining ms), `lifeT` (0-1 normalized), `bounce`, `delayCurrent`, `holdCurrent`. + +Most config properties (speed, scale, alpha, angle, x, y, etc.) accept flexible EmitterOp value formats: +```js +x: 400 // static value +x: [100, 200, 300, 400] // random pick from array +x: { min: 100, max: 700 } // random float in range +x: { min: 100, max: 700, int: true } // random integer +x: { random: [100, 700] } // random integer shorthand +scale: { start: 0, end: 1 } // ease over lifetime (default linear) +scale: { start: 0, end: 1, ease: 'bounce.out' } // custom ease +scale: { start: 4, end: 0.5, random: true } // random start, ease to end +x: { values: [50, 500, 200, 800], interpolation: 'catmull' } // interpolation +x: { steps: 32, start: 0, end: 576 } // stepped sequential +x: { steps: 32, start: 0, end: 576, yoyo: true } // stepped with yoyo +x: { // custom callbacks + onEmit: (particle, key, t, value) => value, + onUpdate: (particle, key, t, value) => value +} +x: (particle, key, t, value) => value + 50 // emit-time callback shorthand +``` +Emit-only properties (no onUpdate): `angle`, `delay`, `hold`, `lifespan`, `quantity`, `speedX`, `speedY`. Emit + Update (support start/end and onUpdate): `accelerationX/Y`, `alpha`, `bounce`, `maxVelocityX/Y`, `moveToX/Y`, `rotate`, `scaleX/Y`, `tint`, `x`, `y`. + +Flow vs Explode: in flow mode (`frequency >= 0`) the emitter emits `quantity` particles every `frequency` ms — default is `frequency: 0` (every frame) with `emitting: true`. In explode mode (`frequency = -1`) it emits a batch all at once, then stops. +```js +emitter.flow(100, 5); // 5 particles every 100ms +emitter.flow(100, 5, 50); // auto-stop after 50 total +emitter.explode(30, 200, 400); // burst 30 at position +emitter.explode(30); // burst at emitter position +``` +</core_concepts> + +<common_patterns> +Scale, alpha, and color over lifetime: +```js +this.add.particles(400, 300, 'spark', { + lifespan: 2000, speed: 100, + scale: { start: 1, end: 0, ease: 'power2' }, + alpha: { start: 1, end: 0, ease: 'cubic.in' } +}); +``` + +Color interpolation — the `color` array interpolates over lifetime and overrides `tint`: +```js +this.add.particles(400, 300, 'spark', { + lifespan: 2000, speed: 100, scale: { start: 0.5, end: 0 }, + color: [0xfacc22, 0xf89800, 0xf83600, 0x9f0404], colorEase: 'quad.out' +}); +``` + +Tinting: +```js +this.add.particles(400, 300, 'spark', { tint: 0xff0000 }); // static +this.add.particles(400, 300, 'spark', { tint: { start: 0xffffff, end: 0xff0000 } }); // over lifetime +``` + +Gravity wells apply inverse-square gravitational force, pulling (or repelling with negative `power`) particles toward a point: +```js +const emitter = this.add.particles(400, 300, 'spark', { + speed: 100, lifespan: 4000, scale: { start: 0.4, end: 0 }, quantity: 2 +}); +const well = emitter.createGravityWell({ x: 400, y: 300, power: 2, epsilon: 100, gravity: 50 }); +well.x = 300; +well.power = -1; // negative = repel + +// Or create manually and add +const well2 = new Phaser.GameObjects.Particles.GravityWell(500, 200, 3, 100, 50); +emitter.addParticleProcessor(well2); +emitter.removeParticleProcessor(well2); +``` +GravityWell properties: `x`, `y` (world position), `power` (force strength, negative to repel), `epsilon` (min distance for force calc, default 100), `gravity` (gravitational constant, default 50), `active`. Constructor: `new GravityWell(x, y, power, epsilon, gravity)` or `new GravityWell({ x, y, power, epsilon, gravity })`. + +Emission zones (random) — a RandomZone spawns particles at random positions within a shape. The source needs a `getRandomPoint(point)` method; all Phaser geometry classes support it, or provide a custom source: +```js +this.add.particles(400, 300, 'spark', { + speed: 50, lifespan: 2000, + emitZone: { type: 'random', source: new Phaser.Geom.Circle(0, 0, 100) } +}); +emitter.addEmitZone({ + type: 'random', + source: { + getRandomPoint: (point) => { + const a = Math.random() * Math.PI * 2; + point.x = Math.cos(a) * 100; + point.y = Math.sin(a) * 50; + return point; + } + } +}); +``` + +Emission zones (edge) — an EdgeZone places particles sequentially along shape edges. The source needs `getPoints(quantity, stepRate)`; Curves, Paths, and all geometry shapes support it: +```js +this.add.particles(400, 300, 'spark', { + lifespan: 1500, speed: 20, + emitZone: { + type: 'edge', + source: new Phaser.Geom.Circle(0, 0, 150), + quantity: 48, // number of points on edge (use 0 with stepRate instead) + yoyo: false, // reverse direction at ends + seamless: true // remove duplicate endpoint + } +}); +emitter.addEmitZone({ type: 'edge', source: geom, quantity: 50, yoyo: false, seamless: true }); +``` +Multiple emission zones: pass an array to `emitZone` or call `addEmitZone()` repeatedly. Zones iterate in sequence; the `total` property controls how many particles emit before rotating to the next zone (-1 = never rotate). +```js +this.add.particles(400, 300, 'spark', { + emitZone: [ + { type: 'random', source: new Phaser.Geom.Circle(0, 0, 50) }, + { type: 'random', source: new Phaser.Geom.Circle(200, 0, 50) } + ] +}); +``` + +Death zones kill particles on entering (or leaving) a region. The source needs a `contains(x, y)` method: +```js +// Kill particles entering a rectangle +this.add.particles(400, 100, 'spark', { + speed: 200, lifespan: 5000, gravityY: 100, + deathZone: { type: 'onEnter', source: new Phaser.Geom.Rectangle(300, 400, 200, 50) } +}); +// Kill particles leaving a circle (confine to area) +this.add.particles(400, 300, 'spark', { + speed: 100, lifespan: 5000, + deathZone: { type: 'onLeave', source: new Phaser.Geom.Circle(400, 300, 150) } +}); +emitter.addDeathZone({ type: 'onEnter', source: { contains: (x, y) => x > 600 && y > 400 } }); +``` + +Following a game object: +```js +const player = this.add.sprite(100, 100, 'player'); +const emitter = this.add.particles(0, 0, 'spark', { speed: 50, lifespan: 800, scale: { start: 0.5, end: 0 } }); +emitter.startFollow(player); // follow position +emitter.startFollow(player, 10, -20); // with offset +emitter.startFollow(player, 0, 0, true); // track visibility too +emitter.stopFollow(); +// Or via config: +this.add.particles(0, 0, 'spark', { follow: player, followOffset: { x: 0, y: -20 } }); +``` + +Particle callbacks and iteration: +```js +const emitter = this.add.particles(400, 300, 'spark', { + speed: 100, lifespan: 2000, + emitCallback: (particle, emitter) => { /* on emit */ }, + deathCallback: (particle) => { /* on death */ } +}); +emitter.onParticleEmit((particle, emitter) => { /* ... */ }); +emitter.onParticleDeath((particle) => { /* ... */ }); +emitter.forEachAlive((particle, emitter) => { /* particle.x, particle.lifeT */ }); +``` + +Duration, stopAfter, and advance (pre-warm): +```js +this.add.particles(400, 300, 'spark', { speed: 100, duration: 3000 }); // auto-stop after 3s; alive particles finish +this.add.particles(400, 300, 'spark', { speed: 100, stopAfter: 50 }); // emit exactly 50 then stop +this.add.particles(400, 300, 'spark', { speed: 100, lifespan: 2000, advance: 2000 }); // fast-forward 2s on creation +// Or manually: emitter.fastForward(2000, 50); +``` + +Particle bounds (bounce): +```js +this.add.particles(400, 300, 'spark', { + speed: 200, lifespan: 5000, bounce: 0.8, + bounds: { x: 100, y: 100, width: 600, height: 400 }, + collideLeft: true, collideRight: true, collideTop: true, collideBottom: true +}); +// Or: emitter.addParticleBounds(100, 100, 600, 400); +``` + +Texture frames and animations: +```js +this.add.particles(400, 300, 'flares', { frame: ['red', 'green', 'blue'] }); // random frame per particle +this.add.particles(400, 300, 'flares', { frame: { frames: ['red', 'green', 'blue'], cycle: true, quantity: 4 } }); // sequential cycling +this.add.particles(400, 300, 'explosion', { anim: 'explode_anim', lifespan: 1000 }); // play anim over lifetime +this.add.particles(400, 300, 'sheet', { anim: { anims: ['fire', 'smoke'], cycle: false, quantity: 1 } }); // multiple anims, random +``` + +Sorting: +```js +this.add.particles(400, 300, 'spark', { sortProperty: 'y', sortOrderAsc: true }); +// Or: sortCallback: (a, b) => a.y - b.y +``` + +Custom particle processor — extend `ParticleProcessor` for per-particle logic each frame via `update(particle, delta, step, t)`: +```js +class WindProcessor extends Phaser.GameObjects.Particles.ParticleProcessor { + constructor (windX, windY) { super(0, 0); this.windX = windX; this.windY = windY; } + update (particle, delta, step, t) { + particle.velocityX += this.windX * step; + particle.velocityY += this.windY * step; + } +} +emitter.addParticleProcessor(new WindProcessor(0.5, 0)); +``` + +Custom particle class — extend `Particle`, override `update`, and set via `particleClass`: +```js +class TrailParticle extends Phaser.GameObjects.Particles.Particle { + update (delta, step, processors) { + const result = super.update(delta, step, processors); + this.alpha = this.lifeT; // custom: alpha matches life progress + return result; // must return true if particle is still alive + } +} +this.add.particles(400, 300, 'spark', { particleClass: TrailParticle, speed: 100, lifespan: 2000 }); +``` + +Key ParticleEmitter methods — Lifecycle: `start(advance?, duration?)`, `stop(kill?)`, `pause()`, `resume()`, `flow(frequency, count?, stopAfter?)`, `explode(count?, x?, y?)`, `emitParticleAt(x?, y?, count?)`, `emitParticle(count?, x?, y?)`, `fastForward(time, delta?)`. Config: `setConfig`, `updateConfig`. Following: `startFollow`, `stopFollow`. Zones: `addEmitZone`/`removeEmitZone`/`clearEmitZones`, `addDeathZone`/`removeDeathZone`/`clearDeathZones`. Processors: `createGravityWell`, `addParticleProcessor`, `removeParticleProcessor`, `getProcessors`. Bounds: `addParticleBounds`. Callbacks/iteration: `onParticleEmit`, `onParticleDeath`, `killAll`, `forEachAlive`, `forEachDead`. Counts: `getAliveParticleCount`, `getDeadParticleCount`, `getParticleCount`, `atLimit`, `reserve`. Property setters: `setParticleSpeed`, `setParticleScale`, `setParticleGravity`, `setParticleAlpha`, `setParticleTint`, `setParticleLifespan`, `setEmitterAngle`, `setQuantity`, `setFrequency`, `setRadial`, `setEmitterFrame`, `setAnim`. Sorting: `setSortProperty`, `setSortCallback`, `depthSort`. Utility: `getBounds`, `overlap`. +</common_patterns> + +<events> +All events are emitted on the `ParticleEmitter` instance: +- `'start'` `(emitter)` — `start()` is called and the emitter begins emitting +- `'stop'` `(emitter)` — `stop()` is called, or a duration/stopAfter limit is reached +- `'complete'` `(emitter)` — the final alive particle dies after the emitter has stopped +- `'explode'` `(emitter, particle)` — `explode()` is called +- `'deathzone'` `(emitter, particle, zone)` — a death zone kills a particle + +```js +emitter.on('stop', (emitter) => { /* stopped emitting */ }); +emitter.on('complete', (emitter) => { /* all particles dead */ }); +emitter.on('deathzone', (emitter, particle, zone) => { /* ... */ }); +``` +</events> + +<gotchas> +- No ParticleEmitterManager — removed in v3.60. `this.add.particles()` returns a `ParticleEmitter` directly. +- `speed` vs `speedX`/`speedY`: `speed` sets speedX and deactivates speedY (radial mode). `speedX`/`speedY` switches to point mode (`radial: false`). +- `scale` vs `scaleX`/`scaleY`: `scale` applies to scaleX and deactivates scaleY. Use both for non-uniform scaling. +- `color` overrides `tint` — they're mutually exclusive; the color array wins. +- `moveToX`/`moveToY`: both must be set to activate; overrides `angle` and `speed`. +- `emitting` vs `active`: `emitting: false` stops new particles but alive ones still update; `active: false` freezes the entire emitter. +- `stop` vs `complete`: `'stop'` fires when emission stops; `'complete'` fires when the last alive particle dies. +- `frequency: 0` means emit every frame (max rate), not "never." Use `emitting: false` to prevent emission. +- `frequency: -1` puts the emitter in explode mode — it won't flow automatically; use `explode()` for bursts. +- `hold` keeps a particle visible and frozen for the given ms after its lifespan expires, before it dies — useful for trail/lingering effects. +- `advance` fast-forwards by simulating the given ms on creation so particles are already visible on the first frame. +- `reserve(count)` (or `reserve` in config) pre-allocates particle objects upfront, avoiding GC spikes during gameplay. +- Zone source method requirements: RandomZone needs `getRandomPoint(point)`, EdgeZone needs `getPoints(quantity, stepRate)`, DeathZone needs `contains(x, y)`. +- `maxParticles` limits total objects (not alive count); use `maxAliveParticles` for the visible limit. +- The emitter needs a valid texture key; use the `frame` config for multi-frame textures. +</gotchas> +</particles> diff --git a/skills/base44-platform-games/references/phaser/physics_arcade.md b/skills/base44-platform-games/references/phaser/physics_arcade.md new file mode 100644 index 0000000..48d0ad9 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/physics_arcade.md @@ -0,0 +1,256 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: physics arcade + +<physics_arcade> +Use this when working with Arcade Physics in Phaser 4: enabling physics, velocity, acceleration, gravity, collisions (collide/overlap), world bounds, physics groups, static bodies, body properties, and collision categories. + +<quick_start> +Enable Arcade Physics in the game config, then create physics-enabled sprites via `this.physics.add`. + +```js +class GameScene extends Phaser.Scene { + create() { + // Physics sprite (dynamic body, affected by gravity) + this.player = this.physics.add.sprite(100, 300, 'player'); + this.player.setCollideWorldBounds(true); + this.player.setBounce(0.2); + + // Static group (immovable platforms) + this.platforms = this.physics.add.staticGroup(); + this.platforms.create(400, 568, 'ground').setScale(2).refreshBody(); + + // Register a persistent collider (checked every frame automatically) + this.physics.add.collider(this.player, this.platforms); + + this.cursors = this.input.keyboard.createCursorKeys(); + } + + update() { + if (this.cursors.left.isDown) { + this.player.setVelocityX(-160); + } else if (this.cursors.right.isDown) { + this.player.setVelocityX(160); + } else { + this.player.setVelocityX(0); + } + } +} + +// Enable Arcade Physics in game config +const config = { + type: Phaser.AUTO, + width: 800, + height: 600, + physics: { + default: 'arcade', + arcade: { + gravity: { y: 300 }, + debug: false + } + }, + scene: GameScene +}; + +const game = new Phaser.Game(config); +``` +</quick_start> + +<core_concepts> +The `Phaser.Physics.Arcade.World` (`this.physics.world`) manages all bodies. Key properties: `gravity` (Vector2 applied to all dynamic bodies each step — set via config or `world.gravity.y = 300`), `bounds` (Rectangle, defaults to canvas size), `bodies` / `staticBodies` (Sets of bodies), `colliders` (registered Collider objects), `fps` (steps/sec, default 60, change via `world.setFPS(120)`), `fixedStep` (true = fixed timestep, false = sync to render fps), `timeScale` (1.0 normal, 2.0 = half speed, 0.5 = double), `isPaused` (toggle via `world.pause()` / `world.resume()`), `useTree` (RTree spatial index, disable for 5000+ bodies), `drawDebug` (set via config `debug: true`). + +A dynamic `Body` is created automatically when you use `this.physics.add.sprite()` or `.image()`; access it via `gameObject.body`. Key body properties: `velocity` (px/sec), `acceleration` (px/sec^2), `drag` (deceleration, applied only when acceleration is zero), `gravity` (per-body, added to world gravity), `bounce` (rebound factor relative to 1), `friction` (default `(1, 0)`, velocity imparted by immovable body to riding body), `maxVelocity` (default `(10000, 10000)`) / `maxSpeed` (scalar cap, default -1 = none), `mass` (default 1, affects collision momentum), `immovable` (if true, never moved by collisions), `pushable` (if false, reflects velocity to colliding body), `moves` (if false, position not updated by physics), `enable` (false removes from simulation), `useDamping` (when true, drag is a 0-1 multiplier instead of linear), `collideWorldBounds`, and the event opt-ins `onWorldBounds` / `onCollide` / `onOverlap`. + +Body shape: `isCircle` (via `setCircle`), `width`/`height`, `offset` (Vector2), `center` (read-only midpoint). Collision state (read-only, reset each step): `touching` / `blocked` / `wasTouching` — each `{ none, up, down, left, right }`; `embedded` (both overlapping with zero velocity). Angular: `angularVelocity` (deg/sec), `angularAcceleration`, `angularDrag`, `maxAngular` (default 1000), `allowRotation` (default true). + +A `StaticBody` never moves and is unaffected by gravity or velocity; it uses an optimized RTree for fast spatial lookups. Created via `this.physics.add.staticSprite()`, `.staticImage()`, or `.staticGroup()`. After changing the parent Game Object's position or scale, call `body.reset()` or `gameObject.refreshBody()` to sync — static bodies do not auto-sync. They have `collisionCategory` and `collisionMask` like dynamic bodies but no velocity, acceleration, drag, or gravity. +</core_concepts> + +<common_patterns> +Enabling physics on existing Game Objects: +```js +this.physics.add.existing(mySprite); // dynamic body +this.physics.add.existing(mySprite, true); // static body + +// Or via the world directly +this.physics.world.enable(mySprite); // dynamic +this.physics.world.enable(mySprite, Phaser.Physics.Arcade.STATIC_BODY); +``` + +Creating physics sprites and images: +```js +const player = this.physics.add.sprite(100, 200, 'player'); // dynamic body +const coin = this.physics.add.image(300, 100, 'coin'); // dynamic body +const wall = this.physics.add.staticImage(400, 300, 'wall'); // static body +const platform = this.physics.add.staticSprite(400, 500, 'plat'); // static body + +// Standalone bodies (no Game Object) +const sensor = this.physics.add.body(200, 200, 32, 32); // dynamic Body +const zone = this.physics.add.staticBody(100, 100, 64, 64); // static Body +``` + +Velocity and gravity: +```js +player.setVelocity(200, -300); // x=200 px/s, y=-300 px/s (upward) +player.setVelocityX(200); +player.body.velocity.set(200, -300); // equivalent via Vector2 + +player.setAcceleration(100, 0); +player.setMaxVelocity(300, 600); + +player.body.gravity.y = 200; // per-body gravity (added to world gravity) +player.body.allowGravity = false; // exempt from world gravity + +player.setDrag(300); // linear deceleration (acceleration must be zero) +player.body.useDamping = true; +player.setDrag(0.05); // damping mode: keeps 5% velocity/sec + +player.setBounce(0.5); // both axes +player.setBounceY(1); // full vertical bounce +``` + +Collide and overlap — two approaches: persistent Colliders (checked every frame automatically, preferred) or one-shot checks (called manually in `update()`): +```js +// --- Persistent Colliders --- +const collider = this.physics.add.collider(player, platforms); +const overlap = this.physics.add.overlap(player, coins, collectCoin, null, this); + +function collectCoin (player, coin) { + coin.disableBody(true, true); // disable physics and hide +} + +collider.active = false; // temporarily disable +collider.destroy(); // remove permanently + +// With processCallback (must return boolean to allow collision) +this.physics.add.collider(player, enemies, onHit, canCollide, this); +function canCollide (player, enemy) { + return !player.getData('invincible'); +} + +// --- One-shot checks (in update) --- +this.physics.collide(player, platforms); +this.physics.overlap(player, coins, collectCoin, null, this); + +// Self-collision within a single group +this.physics.add.collider(enemies, enemies); +``` +`collide` performs separation (pushes bodies apart); `overlap` only detects intersection without moving anything — use `overlap` for triggers like pickups. + +Groups — dynamic group members get dynamic bodies automatically; static group members get static bodies: +```js +const bullets = this.physics.add.group({ + classType: Phaser.Physics.Arcade.Sprite, // default: ArcadeSprite + maxSize: 20, + collideWorldBounds: true, + bounceX: 1, + bounceY: 1, + velocityX: 200, + allowGravity: false, + immovable: false +}); + +const platforms = this.physics.add.staticGroup(); +platforms.create(400, 568, 'ground'); +platforms.create(600, 400, 'ground'); + +// After modifying a static group member's transform, refresh: +platforms.create(400, 568, 'ground').setScale(2).refreshBody(); + +// Group velocity helpers +bullets.setVelocity(200, 0); // all members +bullets.setVelocityX(200); +bullets.setVelocityY(0, 10); // with step increment per member +``` +PhysicsGroup config keys (applied as defaults to new members at creation only — changing them later does NOT retroactively update existing members): `collideWorldBounds`, `bounceX/Y`, `accelerationX/Y`, `dragX/Y`, `gravityX/Y`, `frictionX/Y`, `velocityX/Y`, `angularVelocity`, `angularAcceleration`, `angularDrag`, `maxVelocityX/Y`, `maxSpeed`, `mass`, `immovable`, `allowDrag`, `allowGravity`, `allowRotation`, `useDamping`, `enable`. + +World bounds: +```js +this.physics.world.setBounds(0, 0, 1600, 1200, true, true, false, true); +this.physics.world.setBoundsCollision(true, true, false, true); // which edges collide + +player.setCollideWorldBounds(true); +player.body.setBoundsRectangle(new Phaser.Geom.Rectangle(100, 100, 600, 400)); +player.body.worldBounce = new Phaser.Math.Vector2(0.5, 0.5); + +// Detect world bounds hit via event (requires opt-in) +player.body.onWorldBounds = true; +this.physics.world.on('worldbounds', (body, up, down, left, right) => { + console.log('Hit edge:', { up, down, left, right }); +}); +``` + +Body properties: +```js +player.body.setSize(24, 32, true); // width, height, re-center on GO +player.body.setOffset(4, 0); // offset from GO position +player.body.setCircle(16); // circular body, radius 16 +player.body.setCircle(16, 4, 4); // circular with offset + +player.body.setImmovable(true); // not moved by collisions +player.body.setPushable(false); // reflects velocity to collider +player.body.slideFactor.set(0, 0); // Sokoban-style: stops after push +player.body.setMass(2); // affects momentum exchange + +player.disableBody(true, true); // disable body + hide Game Object +player.enableBody(true, x, y, true, true); // re-enable at position + show + +player.body.checkCollision.up = false; // don't collide from above +player.body.syncBounds = true; // auto-sync size to texture +``` + +Collision categories filter which bodies can collide using bitmask values (max 32 categories): +```js +const CAT_PLAYER = this.physics.nextCategory(); // 0x0002 +const CAT_ENEMY = this.physics.nextCategory(); // 0x0004 +const CAT_BULLET = this.physics.nextCategory(); // 0x0008 + +player.setCollisionCategory(CAT_PLAYER); +enemy.setCollisionCategory(CAT_ENEMY); +bullet.setCollisionCategory(CAT_BULLET); + +player.setCollidesWith([CAT_ENEMY]); // player hits enemies only +enemy.setCollidesWith([CAT_PLAYER, CAT_BULLET]); // enemies hit player + bullets +bullet.setCollidesWith([CAT_ENEMY]); + +player.addCollidesWith(CAT_BULLET); +player.removeCollidesWith(CAT_ENEMY); +player.willCollideWith(CAT_ENEMY); // returns boolean +player.resetCollisionCategory(); // reset to default (collides with everything) + +// Works on Groups too +enemies.setCollisionCategory(CAT_ENEMY); +enemies.setCollidesWith([CAT_PLAYER, CAT_BULLET]); +``` +Default: all bodies have category `0x0001` and mask `1`, so everything collides because `(1 & 1) !== 0`. PhysicsGroup defaults to mask `2147483647` (all bits). After changing categories, call `resetCollisionCategory()` to set the mask back to all bits. +</common_patterns> + +<events> +All events are emitted on `this.physics.world` and require opt-in per body (set `body.onCollide`, `body.onOverlap`, or `body.onWorldBounds` to `true`): + +- `'collide'` (COLLIDE) — two bodies collide and at least one has `onCollide = true`. Args `(gameObject1, gameObject2, body1, body2)`. +- `'overlap'` (OVERLAP) — two bodies overlap and at least one has `onOverlap = true`. Args `(gameObject1, gameObject2, body1, body2)`. +- `'worldbounds'` (WORLD_BOUNDS) — body hits a world edge and has `onWorldBounds = true`. Args `(body, up, down, left, right)`. +- `'tilecollide'` / `'tileoverlap'` — body collides/overlaps a tile. Args `(gameObject, tile, body)`. +- `'worldstep'` — after each physics step. Args `(delta)`. +- `'pause'` / `'resume'` — world paused/resumed. No args. + +Useful scene-plugin helpers: `this.physics.accelerateTo`, `moveTo`, `velocityFromAngle`, `velocityFromRotation`, `closest`, `furthest`, `overlapRect`, `overlapCirc`. +</events> + +<gotchas> +1. `debug: true` in production — debug rendering is expensive. Always disable for release builds. +2. Static body transform changes — after changing position, scale, or origin of a static body's Game Object, you must call `body.reset()` or `gameObject.refreshBody()`. Static bodies do not auto-sync. +3. `collide` vs `overlap` — `collide` performs separation (pushes bodies apart); `overlap` only detects intersection. Use `overlap` for triggers like pickups. +4. Persistent Collider vs one-shot — `this.physics.add.collider()` is checked every frame automatically; `this.physics.collide()` is a one-shot check you must call each frame in `update()`. Prefer persistent Colliders. +5. Events require opt-in — world events fire only if the relevant body property (`onCollide`, `onOverlap`, `onWorldBounds`) is `true`. +6. Collision categories default — all bodies default to category `0x0001` / mask `1` (PhysicsGroup defaults to mask `2147483647`). Use `this.physics.nextCategory()` for new values; max 32 categories. After changing categories, call `resetCollisionCategory()` to set the mask to all bits. +7. Group defaults only apply at creation — changing a PhysicsGroup's `defaults` later does not retroactively update existing members. +8. `customUpdate: true` in the arcade config stops the world from auto-updating on the Scene UPDATE event — you must call `this.physics.world.update(time, delta)` manually. +9. `useDamping` drag values should be small (e.g. `0.05`) since they act as a multiplier — `0.05` means the body keeps 5% of its velocity per second. +10. Immovable vs pushable — `immovable = true` means never moved by collisions at all; `pushable = false` means it reflects velocity back to the colliding body but can still be separated. +11. Overlap with TilemapLayer — when using `overlapOnly` with a TilemapLayer, every tile is checked regardless of collision settings on individual tiles. +12. RTree threshold — the RTree spatial index (enabled by default) becomes expensive to rebuild with very large numbers of dynamic bodies. Consider `useTree: false` for 5000+ dynamic bodies. +</gotchas> +</physics_arcade> diff --git a/skills/base44-platform-games/references/phaser/physics_matter.md b/skills/base44-platform-games/references/phaser/physics_matter.md new file mode 100644 index 0000000..05f53f6 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/physics_matter.md @@ -0,0 +1,323 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: physics matter + +<physics_matter> +Use this when working with Matter.js physics in Phaser 4: rigid bodies, compound bodies, constraints (joints/springs), composites, sensors, collision filtering, pointer dragging, tilemap integration, and debug rendering. + +<quick_start> +Enable Matter physics in the game config, then create bodies via `this.matter.add`. + +```js +class GameScene extends Phaser.Scene { + create() { + // Matter sprite (dynamic, has animation support) + this.player = this.matter.add.sprite(400, 200, 'player'); + this.player.setBounce(0.5); + this.player.setFriction(0.05); + + // Matter image (dynamic, no animation) + const box = this.matter.add.image(300, 100, 'crate'); + + // Static body from raw shape + this.matter.add.rectangle(400, 580, 800, 40, { isStatic: true }); + + // Enable pointer dragging on all bodies + this.matter.add.mouseSpring(); + + this.cursors = this.input.keyboard.createCursorKeys(); + } + + update() { + if (this.cursors.left.isDown) { + this.player.setVelocityX(-5); + } else if (this.cursors.right.isDown) { + this.player.setVelocityX(5); + } + if (this.cursors.up.isDown && this.player.body.velocity.y > -0.1) { + this.player.setVelocityY(-10); + } + } +} + +// Enable Matter physics in game config +const config = { + type: Phaser.AUTO, + width: 800, + height: 600, + physics: { + default: 'matter', + matter: { + gravity: { y: 1 }, + enableSleeping: true, + debug: true, + setBounds: true // walls around canvas edges + } + }, + scene: GameScene +}; + +const game = new Phaser.Game(config); +``` +</quick_start> + +<core_concepts> +The `MatterPhysics` class is the scene-level plugin (`this.matter`). Key properties: `this.matter.add` (Factory for creating bodies, constraints, and Game Objects, auto-added to world), `this.matter.world` (the `World` instance managing the engine, bounds, and debug rendering; extends EventEmitter), `this.matter.body` / `bodies` / `composite` / `composites` / `constraint` (direct references to Matter.js modules for low-level use), and `this.matter.bodyBounds` (helper for aligning bodies by visual bounds). + +The `World` (`this.matter.world`) exposes `engine` (the `MatterJS.Engine`), `localWorld` (the `MatterJS.World` composite containing all bodies/constraints), `enabled` (false pauses simulation), `autoUpdate` (true = engine updates each game step), and `walls` (`{ left, right, top, bottom }` boundary wall bodies, or null). World config passed under `physics.matter`: `gravity` (default `{ x: 0, y: 1 }`, set `false` to disable), `setBounds` (`true` or `{ x, y, width, height, thickness, left, right, top, bottom }`), `enableSleeping` (allow bodies to sleep when at rest), `positionIterations` (default 6), `velocityIterations` (default 4), `constraintIterations` (default 2), `timing.timeScale` (1 normal, 0 freezes, 0.5 slow-motion), `autoUpdate` (default true), `debug` (`true` or a MatterDebugConfig object), `runner` (use `runner.fps` for fixed timestep). + +Phaser provides two physics-aware Game Object classes plus a function to add physics to any Game Object: `Phaser.Physics.Matter.Sprite` (extends Sprite with all Matter components, supports animations, created via `this.matter.add.sprite(x, y, key, frame, options)`); `Phaser.Physics.Matter.Image` (extends Image, no animation, lighter weight, via `this.matter.add.image(...)`); and `MatterGameObject(world, gameObject, options)` (injects all Matter components into any existing Game Object, via `this.matter.add.gameObject(mySprite, options)`). Both MatterSprite and MatterImage default to a rectangle body matching the texture size — pass `options.shape` to override. + +All Matter Game Objects have these component methods mixed in: Velocity (`setVelocity`, `setVelocityX/Y`, `getVelocity`, `setAngularVelocity`, `setAngularSpeed`, ...), Force (`applyForce`, `applyForceFrom`, `thrust`, `thrustLeft/Right/Back`), Bounce (`setBounce` — restitution 0-1), Friction (`setFriction(value, air?, fstatic?)`, `setFrictionAir`, `setFrictionStatic`), Mass (`setMass`, `setDensity`, `centerOfMass`), Gravity (`setIgnoreGravity`), Sensor (`setSensor`, `isSensor`), Static (`setStatic`, `isStatic`), Sleep (`setToSleep`, `setAwake`, `setSleepThreshold`, `setSleepEvents`), Collision (`setCollisionCategory`, `setCollisionGroup`, `setCollidesWith`, `setOnCollide`, `setOnCollideEnd`, `setOnCollideActive`, `setOnCollideWith`), and SetBody (`setRectangle`, `setCircle`, `setPolygon`, `setTrapezoid`, `setBody`, `setExistingBody`). +</core_concepts> + +<common_patterns> +Runtime setup and world configuration: +```js +this.matter.world.setGravity(0, 2); // x, y, scale (default 0.001) +this.matter.world.disableGravity(); +this.matter.world.setBounds(0, 0, 1600, 1200, 64, true, true, true, true); + +this.matter.set60Hz(); // fixed timestep +this.matter.world.autoUpdate = false; // then manual: this.matter.step(16.666); +this.matter.pause(); +this.matter.resume(); +``` + +Creating Matter sprites and images: +```js +const player = this.matter.add.sprite(200, 300, 'hero'); // default rect body matching texture + +// Custom body shapes via options.shape +const ball = this.matter.add.image(400, 100, 'ball', null, { shape: { type: 'circle', radius: 24 } }); +const hex = this.matter.add.sprite(300, 100, 'hex', null, { shape: { type: 'polygon', sides: 6, radius: 32 } }); +const ship = this.matter.add.sprite(400, 200, 'ship', null, { + shape: { type: 'fromVerts', verts: '0 0 40 0 40 40 20 60 0 40' } +}); + +// PhysicsEditor shape data +const shapes = this.cache.json.get('shapes'); +const enemy = this.matter.add.sprite(500, 200, 'enemy', null, { shape: shapes.enemy }); + +// Add Matter physics to an existing Game Object +const existingSprite = this.add.sprite(100, 100, 'box'); +this.matter.add.gameObject(existingSprite, { restitution: 0.8 }); +``` + +Body config options (`MatterBodyConfig`) — pass as the `options` parameter to any factory method or Matter Game Object: `label`, `isStatic`, `isSensor`, `angle` (radians), `timeScale`, `ignoreGravity`, `ignorePointer`, `density` (0.001 default, auto-calculates mass), `mass`, `restitution` (bounce 0-1), `friction` (0-1), `frictionAir`, `frictionStatic`, `slop` (overlap tolerance), `chamfer` (`{ radius: 5 }` for rounded corners), `collisionFilter: { category, mask, group }`, `onCollideCallback` / `onCollideEndCallback` / `onCollideActiveCallback`, and `shape` (for Game Objects). + +Velocity, forces, and thrust — note forces use very small values (`0.01`-`0.1`) and velocity is in units per step, not pixels/sec: +```js +sprite.setVelocity(3, -5); +sprite.setVelocityX(-3); +sprite.setAngularVelocity(0.05); +const vel = sprite.getVelocity(); // { x, y } + +sprite.applyForce({ x: 0.05, y: 0 }); +sprite.applyForceFrom(position, { x: 0.02, y: -0.02 }); + +sprite.thrust(0.05); // forward (relative to body angle) +sprite.thrustBack(0.05); +sprite.thrustLeft(0.03); +sprite.thrustRight(0.03); + +// Batch operations via this.matter +this.matter.setVelocity(arrayOfBodies, 2, -3); +this.matter.applyForce(arrayOfBodies, { x: 0.01, y: 0 }); +``` + +Constraints (joints and springs) — `constraint(bodyA, bodyB, length?, stiffness?, options?)`, aliases `joint` / `spring`: +```js +const rigid = this.matter.add.constraint(bodyA, bodyB, 100, 1); // rigid joint +const spring = this.matter.add.constraint(bodyA, bodyB, 200, 0.02, { damping: 0.05 }); +const pin = this.matter.add.constraint(bodyA, bodyB, 0, 0.9); // pin joint + +// World constraint (one body pinned to a world point) +this.matter.add.worldConstraint(body, 50, 0.5, { pointA: { x: 400, y: 100 } }); + +// Offset attachment points +this.matter.add.constraint(bodyA, bodyB, 80, 1, { + pointA: { x: 20, y: 0 }, // offset from bodyA center + pointB: { x: -20, y: 0 } // offset from bodyB center +}); + +this.matter.getConstraintLength(constraint); // distance between anchor points +this.matter.world.removeConstraint(constraint); +``` + +Composites (stacks, chains, soft bodies): +```js +const stack = this.matter.add.stack(100, 100, 5, 4, 10, 10, (x, y) => { + return this.matter.bodies.rectangle(x, y, 40, 40); +}); +const imageStack = this.matter.add.imageStack('crate', null, 100, 100, 5, 4, 5, 5); +this.matter.add.chain(stack, 0.5, 0, -0.5, 0, { stiffness: 0.7 }); +this.matter.add.mesh(stack, 5, 4, true, { stiffness: 0.5 }); // grid with constraints + cross braces +this.matter.add.softBody(200, 100, 5, 5, 0, 0, true, 10, { friction: 0.1 }, { stiffness: 0.5 }); + +// Pre-built composites +this.matter.add.newtonsCradle(300, 50, 5, 20, 200); +this.matter.add.car(400, 300, 120, 30, 25); +``` + +Compound bodies combine multiple shapes into one body — the first part is the parent. Parts share position, angle, and velocity, and constraints must target the parent, not the parts: +```js +const partA = this.matter.bodies.rectangle(0, 0, 60, 20); +const partB = this.matter.bodies.circle(0, -30, 15); +const compoundBody = this.matter.body.create({ parts: [partA, partB] }); + +const player = this.matter.add.sprite(400, 200, 'hero'); +player.setExistingBody(compoundBody); +``` + +Sleep system — bodies at rest can sleep to skip simulation (requires `enableSleeping: true`): +```js +if (body.isSleeping) { /* body is at rest */ } +sprite.setSleepThreshold(30); // lower = falls asleep faster (default 60) +sprite.setToSleep(); +sprite.setAwake(); +sprite.setSleepEvents(true, true); // enable sleepstart/sleepend events + +this.matter.world.on('sleepstart', (event, body) => {}); +this.matter.world.on('sleepend', (event, body) => {}); +``` + +Sensors detect collisions but do not physically react — useful for trigger zones and pickups. They fire normal collision events (use `collisionstart`/`end` to detect entry/exit): +```js +const trigger = this.matter.add.rectangle(400, 300, 100, 100, { isSensor: true, isStatic: true }); +sprite.setSensor(true); +sprite.isSensor(); +``` + +Collision categories and filtering use bitmasks: `category` (which group this body belongs to, power of 2), `mask` (which categories it collides with), and `group` (shortcut: same positive = always collide, same negative = never collide, 0 = use category/mask): +```js +const PLAYER = this.matter.world.nextCategory(); // 0x0002 (32 max) +const ENEMY = this.matter.world.nextCategory(); // 0x0004 +const GROUND = this.matter.world.nextCategory(); // 0x0008 + +player.setCollisionCategory(PLAYER); +player.setCollidesWith([ENEMY, GROUND]); + +bullet.setCollisionCategory(0x0010); +bullet.setCollidesWith([ENEMY, GROUND]); // bullets skip player + +// Collision groups: same negative = never collide with each other +const noCollide = this.matter.world.nextGroup(true); +spriteA.setCollisionGroup(noCollide); +spriteB.setCollisionGroup(noCollide); + +// Via body config: collisionFilter: { category, mask, group } +// Batch: this.matter.setCollisionCategory([bodyA, bodyB], ENEMY); +``` + +Collision callbacks — per-body callbacks on Matter Game Objects, plus Game Object-level events: +```js +player.setOnCollide((pair) => { /* pair.bodyA, pair.bodyB */ }); +player.setOnCollideEnd((pair) => {}); +player.setOnCollideActive((pair) => {}); +player.setOnCollideWith(enemy, (body, pair) => {}); + +player.on('collide', (bodyA, bodyB, pair) => {}); +player.on('collideEnd', (bodyA, bodyB, pair) => {}); +``` + +Tilemap integration — collision MUST be set first via `setCollisionByProperty` etc.: +```js +const map = this.make.tilemap({ key: 'level' }); +const tileset = map.addTilesetImage('tiles', 'tiles-img'); +const layer = map.createLayer('Ground', tileset, 0, 0); + +layer.setCollisionByProperty({ collides: true }); // MUST set collision first +this.matter.world.convertTilemapLayer(layer); // creates MatterTileBody per colliding tile + +// Uses Tiled collision shapes if defined, otherwise tile bounds. +// Access: tile.physics.matterBody +// Individual tile: this.matter.add.tileBody(tile, { isStatic: true, friction: 0.5 }); +// After map changes: this.matter.world.convertTiles([tile1, tile2]); +``` + +Friction types — Matter has three independent friction values: +```js +sprite.setFriction(0.1); // dynamic: resistance during motion (0-1) +sprite.setFrictionStatic(0.5); // static: resistance before motion starts +sprite.setFrictionAir(0.05); // air: environmental drag (default 0.01) +sprite.setFriction(0.1, 0.02, 0.3); // set all three: dynamic, air, static +``` + +Complex shapes from vertices (concave shapes auto-decomposed): +```js +const body = this.matter.add.fromVertices(400, 300, '0 0 40 0 40 40 20 60 0 40'); + +const vertexSets = [ + [{ x: 0, y: 0 }, { x: 40, y: 0 }, { x: 40, y: 40 }], + [{ x: 40, y: 40 }, { x: 20, y: 60 }, { x: 0, y: 40 }] +]; +this.matter.add.fromVertices(300, 200, vertexSets); +``` + +Pointer constraint (mouse/touch dragging): +```js +const pc = this.matter.add.mouseSpring({ stiffness: 0.2, damping: 0.1 }); +pc.active = false; // disable temporarily +body.ignorePointer = true; // prevent specific body from being dragged +pc.stopDrag(); // release current drag programmatically +pc.destroy(); + +this.matter.world.on('dragstart', (body, part, constraint) => {}); +this.matter.world.on('drag', (body, constraint) => {}); +this.matter.world.on('dragend', (body, constraint) => {}); +``` + +Queries (raycasting and hit testing): +```js +const hits = this.matter.intersectPoint(pointer.x, pointer.y); // bodies at point +const contains = this.matter.containsPoint(body, x, y); // point-in-body test +const inRegion = this.matter.intersectRect(100, 100, 200, 200); // bodies in rect +const rayHits = this.matter.intersectRay(0, 300, 800, 300, 1); // raycast +const colliding = this.matter.intersectBody(playerBody); // body overlap + +this.matter.overlap(playerBody, enemyBodies, (bodyA, bodyB, info) => { + console.log('Overlapping', bodyA, bodyB); +}); +``` + +Debug rendering — pass options as the `debug` property in the matter config. Boolean flags include `showBody`, `showStaticBody`, `showVelocity`, `showCollisions`, `showSensors`, `showJoint`, `showPositions`, `showBounds`, `showAxes`, `showAngleIndicator`, `showSleeping`, `showConvexHulls`, `showInternalEdges`, `renderFill`, `renderLine`; color/style options include `lineColor`, `lineThickness`, `fillColor`, `staticLineColor`, `staticFillColor`, `sensorLineColor`, `jointColor`, `pinColor`, `springColor`, `anchorColor`, `positionColor`: +```js +this.matter.world.drawDebug = false; // toggle at runtime +this.matter.world.debugGraphic.visible = false; + +this.matter.world.setBodyRenderStyle(body, 0xff0000, 1, 2, 0x00ff00, 0.5); +this.matter.world.setConstraintRenderStyle(constraint, 0xffff00, 1, 2); +``` +</common_patterns> + +<events> +All events are emitted on `this.matter.world` (which extends EventEmitter): + +- `'collisionstart'` `(event, bodyA, bodyB)` — two bodies first start colliding. +- `'collisionactive'` `(event, bodyA, bodyB)` — two bodies are still colliding. +- `'collisionend'` `(event, bodyA, bodyB)` — two bodies stop colliding. +- `'beforeupdate'` / `'afterupdate'` `(event)` — before/after each engine update step. +- `'beforeadd'` / `'afteradd'` / `'beforeremove'` / `'afterremove'` `(event)` — around body/constraint add/remove. +- `'dragstart'` `(body, part, constraint)`, `'drag'` `(body, constraint)`, `'dragend'` `(body, constraint)` — pointer dragging. +- `'sleepstart'` / `'sleepend'` `(event, body)` — body sleeps/wakes (requires `setSleepEvents`). +- `'pause'` / `'resume'` — world paused/resumed. No args. + +Collision events include `event.pairs` — an array of collision pair objects with `bodyA`, `bodyB`, collision depth, and normal. +</events> + +<gotchas> +- Force values are tiny. Use `0.01`-`0.1` for forces, `1`-`15` for velocity. Not pixel-based. +- `setBody`/`setRectangle`/etc. resets all properties — mass, friction, collision filters, callbacks are wiped. Re-apply after changing shape. +- Constraints must target the parent body, not compound body `parts`. +- 32 collision categories max. Each `nextCategory()` uses one bit. +- `collisionFilter.group` overrides category/mask. Same positive = always collide; same negative = never collide; zero/different = use category/mask. +- Sensors still need matching collision filters to fire events. +- Matter position is the center of mass, not top-left (unlike Arcade Physics). +- Sleep events require opt-in: `sprite.setSleepEvents(true, true)`. +- Tilemap conversion requires collision set first via `setCollisionByProperty` etc. +- Restitution uses `Math.max(bodyA.restitution, bodyB.restitution)` — the bouncier value wins. +- `body.ignorePointer = true` prevents the pointer constraint from dragging that body. +</gotchas> +</physics_matter> diff --git a/skills/base44-platform-games/references/phaser/render_textures.md b/skills/base44-platform-games/references/phaser/render_textures.md new file mode 100644 index 0000000..08a7f57 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/render_textures.md @@ -0,0 +1,245 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: render textures + +<render_textures> +Use this when drawing game objects to off-screen textures in Phaser 4: the RenderTexture game object, DynamicTexture for shared textures, the Stamp helper, command-buffer rendering, snapshots, procedural generation, and minimaps. + +<quick_start> +```js +// In a Scene's create() method: + +// 1. RenderTexture -- a visible game object with its own DynamicTexture +const rt = this.add.renderTexture(400, 300, 256, 256); +rt.draw('player', 128, 128); // draw a texture by key at center +rt.fill(0x222244, 0.5); // semi-transparent fill +rt.render(); // flush the command buffer + +// 2. DynamicTexture -- a shared texture in the Texture Manager +const dt = this.textures.addDynamicTexture('composite', 512, 512); +dt.stamp('coin', null, 64, 64, { scale: 2, angle: 45 }); +dt.render(); +this.add.image(400, 300, 'composite'); // any game object can use it + +// 3. Stamp game object -- lightweight Image that ignores camera scroll +const hud = this.add.stamp(10, 10, 'heart'); +``` +</quick_start> + +<core_concepts> +Phaser 4 splits texture-drawing into two layers. A **RenderTexture** is an Image game object with an auto-created DynamicTexture — created via `this.add.renderTexture(x, y, w, h)`, visible on its own, has position/scale/alpha (all Image components), but belongs to one Scene (no cross-scene use; share across objects via `saveTexture(key)`). A **DynamicTexture** is a texture in the Texture Manager — created via `this.textures.addDynamicTexture(key, w, h)`, not visible on its own (must be assigned to a game object), shared by key, and usable cross-scene since textures are global. + +Use **RenderTexture** when you need a single visible surface you draw onto (paint canvas, trail effect, composite sprite). Use **DynamicTexture** when many game objects share the same generated texture, you need a texture for masks/shaders, or you need cross-scene access. + +RenderTexture is a thin proxy — `draw()`, `stamp()`, `fill()`, `clear()`, `erase()`, `snapshot()` all delegate to its underlying `this.texture` (a DynamicTexture). It extends Image, so its origin defaults to (0.5, 0.5); call `rt.setOrigin(0, 0)` for top-left positioning (common for full-screen or minimap RTs). + +The command buffer (v4 architecture): drawing calls (`draw`, `stamp`, `fill`, `clear`, `erase`, `repeat`, `capture`) do NOT execute immediately — they push commands into a `commandBuffer` array. You must call `.render()` to flush and execute it: +```js +rt.clear(); +rt.fill(0x000000); +rt.draw(sprite, 128, 128); +rt.render(); // REQUIRED -- nothing appears without this +``` + +For RenderTexture game objects, the `renderMode` property controls automatic rendering: `'render'` (default) draws the texture contents to the frame each tick, you call `render()` manually when content changes; `'redraw'` calls `render()` automatically every frame but does NOT display itself (useful for textures reused by other objects); `'all'` calls `render()` every frame AND draws itself. Set via `rt.setRenderMode('all')` or `rt.setRenderMode('all', true)` (same, plus preserve the command buffer). + +Preserve mode: by default the command buffer clears after `render()`. Call `preserve(true)` to keep commands between renders so the same drawing replays each frame: +```js +rt.preserve(true); +rt.clear(); +rt.draw(sprite); +// On every subsequent render(), clear + draw will repeat +``` + +The `Stamp` game object (`this.add.stamp(x, y, texture, frame)`) is a lightweight Image subclass that ignores camera scroll and transform during rendering. It is used internally by DynamicTexture for drawing operations and is also useful for HUD elements. Note this is different from the `stamp()` method: `rt.stamp(key, frame, x, y, config)` draws a texture frame onto the surface with transform options, while `this.add.stamp(x, y, texture, frame)` creates a Stamp game object on the display list. +</core_concepts> + +<common_patterns> +Drawing sprites and game objects — `draw()` accepts renderable game objects, Groups, Containers, Display Lists, other RenderTextures/DynamicTextures, Texture Frames, texture key strings, or arrays of any of these: +```js +const rt = this.add.renderTexture(0, 0, 800, 600); + +rt.draw(sprite, 100, 100); // single object at offset +rt.draw([sprite1, sprite2, sprite3]); // array +rt.draw(enemyGroup); // group/container (only visible children drawn) +rt.draw(myContainer, 50, 50); // offset added to children positions +rt.draw(this.children); // entire Scene display list +rt.draw('explosion', 200, 200); // texture by string key +rt.render(); +``` +The `alpha` and `tint` parameters on `draw()` only apply to Texture Frames/strings — game objects use their own alpha and tint when drawn. + +Stamping textures with config — the frame is centered on x/y by default (origin 0.5): +```js +rt.stamp('bullet', null, 100, 100, { + alpha: 0.8, + tint: 0xff0000, + angle: 45, // degrees (takes precedence over rotation) + scale: 2, // uniform scale + scaleX: 1.5, // overrides scale for X + scaleY: 0.5, + originX: 0, // top-left origin + originY: 0, + blendMode: 0 +}); +rt.render(); +``` + +Capture — draws a game object with temporary property overrides, restoring originals afterward (useful for drawing at a different position/scale without modifying the object): +```js +rt.capture(player, { + x: 64, + y: 64, + scale: 0.5, + alpha: 0.8, + rotation: Math.PI / 4, + transform: 'world', // 'local', 'world', or a TransformMatrix + camera: someCamera // optional camera override +}); +rt.render(); +``` + +Filling and clearing: +```js +rt.fill(0xff0000); // solid red +rt.fill(0x000000, 0.5); // semi-transparent black +rt.fill(0x00ff00, 1, 10, 10, 100, 50); // green rect at (10,10) size 100x50 +rt.clear(); // clear everything (transparent) +rt.clear(10, 10, 100, 50); // clear a region +rt.render(); +``` + +Erasing uses ERASE blend mode to cut holes: +```js +rt.fill(0xffffff); // white background +rt.erase(circleSprite, 100, 100); // punch a hole shaped like the sprite +rt.render(); +``` + +Repeating / tiling a texture: +```js +rt.repeat('grass', null); // tile the whole surface +rt.repeat('brick', null, 0, 0, 256, 128); // tile a specific region +rt.repeat('tile', 'frame2', 0, 0, 512, 512, { + tileScaleX: 2, + tileScaleY: 2, + tilePositionX: 16, + alpha: 0.8 +}); +rt.render(); +``` + +Snapshots read pixel data from the framebuffer — they are expensive and blocking (use `readPixels`), so use sparingly: +```js +// Full texture snapshot -- callback receives an HTMLImageElement +rt.snapshot(function (image) { + document.body.appendChild(image); +}); + +// Area snapshot +rt.snapshotArea(0, 0, 128, 128, function (image) { + // image is 128x128 region from top-left +}, 'image/jpeg', 0.8); + +// Single pixel -- callback receives a Phaser.Display.Color (cheaper than full snapshot) +rt.snapshotPixel(64, 64, function (color) { + console.log(color.r, color.g, color.b, color.a); +}); +``` + +Using as a texture source for other objects — for a RenderTexture, register it via `saveTexture`; for a DynamicTexture, the key is set at creation: +```js +const rt = this.add.renderTexture(0, 0, 256, 256); +rt.draw(complexScene); +rt.render(); +rt.saveTexture('composited'); // register in the Texture Manager +this.add.image(400, 300, 'composited'); +this.add.sprite(100, 100, 'composited'); +// Updates to the RenderTexture automatically reflect on all users + +const dt = this.textures.addDynamicTexture('terrain', 1024, 1024); +dt.repeat('grass', null); +dt.render(); +this.add.image(512, 512, 'terrain'); // already keyed +``` + +Procedural generation: +```js +const dt = this.textures.addDynamicTexture('starfield', 800, 600); +dt.fill(0x000011); +for (let i = 0; i < 200; i++) { + const x = Phaser.Math.Between(0, 800); + const y = Phaser.Math.Between(0, 600); + const brightness = Phaser.Math.FloatBetween(0.3, 1); + dt.stamp('star', null, x, y, { alpha: brightness, scale: Phaser.Math.FloatBetween(0.5, 1.5) }); +} +dt.render(); +this.add.image(400, 300, 'starfield'); +``` + +Minimap pattern: +```js +create() { + this.minimap = this.add.renderTexture(700, 50, 150, 100); + this.minimap.setScrollFactor(0); // fixed to camera + this.minimap.setRenderMode('all'); // auto-render every frame + this.minimap.preserve(true); +} + +// In update or a callback: +this.minimap.clear(); +this.minimap.fill(0x111111); +this.minimap.capture(player, { x: px * 0.1, y: py * 0.1, scale: 0.1 }); +// render() is automatic in 'all' mode +``` + +Callbacks in the command buffer — insert logic between draw commands that executes during `render()`: +```js +rt.clear(); +rt.draw(background); +rt.callback(() => { sprite.setTint(0xff0000); }); // runs during render, after background +rt.draw(sprite); +rt.callback(() => { sprite.setTint(0xffffff); }); // restore after drawing +rt.render(); +``` + +Resizing erases all content and recreates the framebuffer: +```js +rt.resize(512, 512); +rt.resize(513, 513, false); // disable force-even rounding +``` + +Internal camera — both RenderTexture and DynamicTexture have a `.camera` property that controls how game objects are positioned when drawn. Scroll/zoom/rotate it to transform drawn content. Note `stamp()` ignores the camera; only `draw()` and game object rendering respect it: +```js +rt.camera.setScroll(100, 50); +rt.camera.setZoom(2); +rt.draw(sprite); // sprite drawn with camera transform applied +rt.render(); +``` + +Factory methods: +```js +this.add.renderTexture(x, y, width?, height?) // default 32x32 +this.add.stamp(x, y, texture, frame?) +this.textures.addDynamicTexture(key, width?, height?, forceEven?) // default 256x256 +``` +DynamicTexture also exposes `setSize(w, h?, forceEven?)`, `camera`, `commandBuffer`, `drawingContext` (WebGL DrawingContext with framebuffer), and `getWebGLTexture()`. The `stamp()` config supports `alpha`, `tint`, `angle` (degrees, overrides `rotation` if non-zero), `rotation` (radians), `scale`, `scaleX`/`scaleY`, `originX`/`originY` (default 0.5), `blendMode`. The `capture()` config supports `transform`, `camera`, `x`/`y`/`alpha`/`tint`/`angle`/`rotation`, `scale`/`scaleX`/`scaleY`, `originX`/`originY`, `blendMode`. +</common_patterns> + +<gotchas> +1. Must call `render()`. In Phaser 4 drawing methods buffer commands — nothing appears until `render()` is called (unless using `renderMode: 'all'` or `'redraw'`). +2. Cannot draw to itself. A DynamicTexture/RenderTexture silently skips entries that reference itself. +3. Command buffer clears after render. Call `preserve(true)` before drawing if you need the same commands to replay each frame. +4. `stamp()` ignores the internal camera. Only game objects drawn via `draw()` or `capture()` respect the `.camera` property; texture stamps are positioned absolutely. +5. Default sizes differ. RenderTexture defaults to 32x32; DynamicTexture defaults to 256x256. +6. `forceEven` rounds dimensions up. Width/height are rounded to even numbers for rendering quality by default — pass `false` as the last argument for exact odd dimensions. +7. Snapshots are expensive. They use `readPixels` (WebGL) which blocks the GPU pipeline. Avoid per-frame snapshots; `snapshotPixel` is cheaper than full `snapshot`. +8. WebGL1 has no anti-aliasing on framebuffers. Shapes and Graphics drawn to a RenderTexture may have jagged edges — use pre-rendered sprite textures instead. +9. `resize()` erases content. Resizing destroys and recreates the framebuffer. +10. `saveTexture` renames, does not copy. It assigns the DynamicTexture to the Texture Manager under that key; calling again with a different key renames it. Destroying the RenderTexture without saving first destroys the texture. +11. `draw()` alpha/tint params only affect Texture Frames. When drawing game objects, they use their own alpha and tint. +12. WebGL context loss. DynamicTexture contents are lost on context loss — listen for the `restorewebgl` event to redraw. +13. Groups/Containers: x, y offset is additive. With `rt.draw(group, x, y)`, the offset is added to each child's position — children at (100, 50) drawn with offset (10, 10) appear at (110, 60). +</gotchas> +</render_textures> diff --git a/skills/base44-platform-games/references/phaser/scale_and_responsive.md b/skills/base44-platform-games/references/phaser/scale_and_responsive.md new file mode 100644 index 0000000..971a7d3 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/scale_and_responsive.md @@ -0,0 +1,188 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: scale and responsive + +<scale_and_responsive> +Use this when making a Phaser 4 game responsive or handling display scaling: the ScaleManager, scale modes (FIT, RESIZE, EXPAND, ENVELOP), auto-center, zoom, fullscreen, orientation handling, and browser resize. + +<quick_start> +Scale-to-fit with centering — the most common setup for responsive games: + +```js +const config = { + type: Phaser.AUTO, + scale: { + parent: 'game-container', + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_BOTH, + width: 800, + height: 600 + }, + scene: MyScene +}; + +const game = new Phaser.Game(config); +``` + +The `scale` config is parsed into a `Phaser.Core.Config` instance that the ScaleManager reads during boot. The ScaleManager sets the canvas element size and applies CSS scaling to fit it within its parent. +</quick_start> + +<core_concepts> +The ScaleManager is created during the Game boot sequence and is accessible at `game.scale` or `this.scale` from within a Scene. It extends EventEmitter. + +Three internal Size components drive all calculations: `gameSize` (`game.scale.gameSize`) is the unmodified game dimensions from config, used for world bounds and cameras (read via `game.scale.width` / `.height`); `baseSize` is the auto-rounded gameSize that sets the actual `canvas.width` / `canvas.height` attributes; `displaySize` is the CSS-scaled canvas size after applying scale mode, parent bounds, and zoom, setting `canvas.style.width` / `.height`. Scaling works by keeping the canvas element dimensions fixed (baseSize) and stretching it via CSS (displaySize) — equivalent to CSS `transform-scale` but without prefix issues. The `displayScale` property (Vector2) holds `baseSize / canvasBounds` and is used internally for input coordinate transformation. + +Scale modes (`Phaser.Scale.ScaleModes`, set via `scale.mode`; shorthand constants like `Phaser.Scale.FIT` also exist directly on `Phaser.Scale`): +- `NONE` (0) — no automatic scaling. Canvas uses config width/height; you manage sizing yourself. If you resize the canvas externally, call `game.scale.resize(w, h)` to update internals. +- `WIDTH_CONTROLS_HEIGHT` (1) — height adjusts automatically to maintain aspect ratio based on width. +- `HEIGHT_CONTROLS_WIDTH` (2) — width adjusts automatically to maintain aspect ratio based on height. +- `FIT` (3) — scales to fit inside parent while preserving aspect ratio. May leave empty space (letterbox/pillarbox). Most commonly used. +- `ENVELOP` (4) — scales to cover the entire parent while preserving aspect ratio. May extend beyond parent bounds (content gets cropped). +- `RESIZE` (5) — the canvas element itself is resized to fill parent. No CSS scaling, 1:1 pixel mapping; `gameSize`, `baseSize`, and `displaySize` all change to match parent. Beware GPU fill-rate on large displays. +- `EXPAND` (6) — hybrid of RESIZE and FIT: the visible area resizes to fill the parent, and the canvas scales to fit inside that area. Added in v3.80. + +Center modes (set via `scale.autoCenter`; centering uses CSS `marginLeft`/`marginTop` on the canvas): `NO_CENTER` (0, default), `CENTER_BOTH` (1), `CENTER_HORIZONTALLY` (2), `CENTER_VERTICALLY` (3). The parent element must have calculable bounds — if it has no defined width/height, centering will not work correctly. + +Zoom (set via `scale.zoom`, multiplies the display size): `NO_ZOOM` (1, default), `ZOOM_2X` (2, good for pixel art at low base resolution), `ZOOM_4X` (4), `MAX_ZOOM` (-1, automatically calculates the largest integer zoom that fits the parent). You can also pass any numeric value. Zoom affects CSS display size but not canvas resolution. + +Orientation (read via `game.scale.orientation`, string values): `Phaser.Scale.Orientation.LANDSCAPE` = `'landscape-primary'`, `LANDSCAPE_SECONDARY` = `'landscape-secondary'`, `PORTRAIT` = `'portrait-primary'`, `PORTRAIT_SECONDARY` = `'portrait-secondary'`. Convenience booleans: `isPortrait`, `isLandscape` (device orientation), `isGamePortrait`, `isGameLandscape` (game dimensions). +</core_concepts> + +<common_patterns> +FIT mode with centering (most common) — maintains aspect ratio and centers within the parent; empty space appears as letterbox/pillarbox bars (style the parent's background to control bar appearance): +```js +scale: { + parent: 'game-container', + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_BOTH, + width: 1280, + height: 720 +} +``` + +Responsive resize (dynamic canvas size) — width/height accept percentage strings that resolve against the parent size (falling back to `window.innerWidth`/`innerHeight` if the parent has no size): +```js +scale: { + parent: 'game-container', + mode: Phaser.Scale.RESIZE, + width: '100%', + height: '100%' +} +``` +```js +// In your scene -- respond to size changes: +create() { + this.scale.on('resize', this.handleResize, this); +} + +handleResize(gameSize, baseSize, displaySize) { + this.cameras.resize(gameSize.width, gameSize.height); + // Reposition UI elements based on new dimensions +} +``` + +Fullscreen toggle — must be called from a `pointerup` gesture (not `pointerdown`): +```js +this.input.on('pointerup', () => { + if (this.scale.isFullscreen) { + this.scale.stopFullscreen(); + } else { + this.scale.startFullscreen(); + } +}); + +// Or the convenience method: +this.input.on('pointerup', () => { + this.scale.toggleFullscreen(); +}); +``` +`startFullscreen(fullscreenOptions)` requests browser fullscreen (default options `{ navigationUI: 'hide' }`), `stopFullscreen()` exits, `toggleFullscreen(fullscreenOptions)` toggles, `isFullscreen` is the read-only state. If no `fullscreenTarget` is configured, Phaser creates a temporary `<div>`, moves the canvas into it, and sends that div fullscreen (the div is removed when leaving). For iframes, the `allowfullscreen` attribute is required. + +Mobile orientation handling: +```js +create() { + this.scale.on('orientationchange', (orientation) => { + if (orientation === Phaser.Scale.LANDSCAPE) { + // Show game UI + } else { + // Show "rotate device" message + } + }); + + // Lock orientation (mobile browsers only, limited support): + this.scale.lockOrientation('landscape'); +} +``` + +Fixed size (no scaling) — in NONE mode, if you change the canvas size externally you must call `game.scale.resize(newWidth, newHeight)` to update all internal components including input coordinates: +```js +scale: { + mode: Phaser.Scale.NONE, + width: 800, + height: 600 +} +``` + +Pixel art with max zoom — `MAX_ZOOM` calculates the largest integer multiplier that fits in the parent; combined with `pixelArt: true` (which disables anti-aliasing) this gives crisp pixel rendering: +```js +scale: { + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_BOTH, + width: 320, + height: 240, + zoom: Phaser.Scale.MAX_ZOOM +}, +pixelArt: true +``` + +Snap values for grid alignment — when the browser resizes, dimensions snap down to the nearest grid multiple (floor). Best with FIT mode. Can also be set at runtime via `game.scale.setSnap(16, 16)` or reset with `game.scale.setSnap()`: +```js +scale: { + mode: Phaser.Scale.FIT, + width: 800, + height: 600, + snap: { width: 16, height: 16 } +} +``` + +Min/max size constraints — the display size is clamped to these bounds during scaling calculations: +```js +scale: { + mode: Phaser.Scale.FIT, + width: 800, + height: 600, + min: { width: 400, height: 300 }, + max: { width: 1600, height: 1200 } +} +``` + +Other `scale` config properties: `expandParent` (default true, allows the ScaleManager to set parent/body CSS height to 100%), `parent` (DOM element or its ID; `undefined` = `document.body`, `null` = no parent so you manage canvas placement), `autoRound` (default false, floors display/style sizes for low-powered devices), `resizeInterval` (default 500ms, between parent size checks for fallback polling), `fullscreenTarget` (element to send fullscreen; if unset, Phaser creates a wrapper div). + +Useful runtime methods: `resize(width, height)` (NONE mode only — sets game/base/display sizes directly and updates the canvas + CSS), `setGameSize(width, height)` (scaled modes — changes base game size and re-applies scaling), `setParentSize(width, height)`, `setZoom(value)`, `setMaxZoom()`, `getMaxZoom()`, `setSnap(w, h)`, `getViewPort(camera?, out?)`, `refresh(prevW?, prevH?)` (forces recalculation and emits RESIZE), `transformX(pageX)` / `transformY(pageY)` (DOM-to-game coordinate), `getParentBounds()`, `updateCenter()`, `updateBounds()`. +</common_patterns> + +<events> +All events are on `Phaser.Scale.Events` and are emitted by the ScaleManager (`game.scale`): + +- `'resize'` (RESIZE) — any resize, refresh, or scale change. Args `(gameSize, baseSize, displaySize, previousWidth, previousHeight)`; the three Size params have `.width`, `.height`, `.aspectRatio`. +- `'orientationchange'` (ORIENTATION_CHANGE) — device orientation changes. Args `(orientation)`, a string constant from `Phaser.Scale.Orientation`. +- `'enterfullscreen'` (ENTER_FULLSCREEN) — browser successfully enters fullscreen. +- `'leavefullscreen'` (LEAVE_FULLSCREEN) — browser leaves fullscreen (via code or user ESC). +- `'fullscreenfailed'` (FULLSCREEN_FAILED) — fullscreen request denied. Args `(error)`. +- `'fullscreenunsupported'` (FULLSCREEN_UNSUPPORTED) — browser lacks the Fullscreen API. +</events> + +<gotchas> +1. Parent element must have dimensions. The ScaleManager relies on the parent's computed CSS size. An unstyled `<div>` has zero height, which breaks centering and scaling. Give it explicit CSS dimensions or rely on `expandParent: true` (default) to set body/parent to 100% height. +2. No padding on the parent. The ScaleManager does not account for parent padding. Apply padding to a wrapper element instead, or use margins on the parent's parent. +3. Do not style the canvas directly. The ScaleManager controls `canvas.style.width`, `.height`, `marginLeft`, and `marginTop`. External CSS on the canvas will conflict. +4. Fullscreen requires `pointerup`, not `pointerdown`. On touch devices, `pointerdown` fullscreen requests are blocked unless the document already received touch input. Always use `pointerup`. +5. Fullscreen in iframes needs `allowfullscreen`. Without this attribute on the iframe, fullscreen requests silently fail. +6. RESIZE mode and GPU fill-rate. RESIZE creates a 1:1 pixel canvas matching the parent; on high-resolution displays this can be very large. Monitor performance on low-end devices. +7. `resize()` vs `setGameSize()`. Use `resize()` only in NONE mode (it sets everything directly); use `setGameSize()` with FIT/ENVELOP/etc. (it preserves the scale mode calculations). +8. iOS height quirks. When the parent is the window on iOS, the ScaleManager uses `GetInnerHeight` to work around Safari's dynamic toolbar affecting `getBoundingClientRect`. +9. Percentage strings require a sized parent. `width: '100%'` / `height: '100%'` resolve against `parentSize`; if the parent has no size, they fall back to `window.innerWidth`/`innerHeight`. +10. `resizeInterval` is a fallback poll. Modern browsers dispatch `resize` events, but the ScaleManager also polls every `resizeInterval` ms (default 500) to catch edge cases on older browsers. +</gotchas> +</scale_and_responsive> diff --git a/skills/base44-platform-games/references/phaser/scenes.md b/skills/base44-platform-games/references/phaser/scenes.md new file mode 100644 index 0000000..858f3cf --- /dev/null +++ b/skills/base44-platform-games/references/phaser/scenes.md @@ -0,0 +1,274 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: scenes + +<scenes> +Use this when working with Phaser 4 scenes: scene lifecycle methods, transitions, parallel scenes, scene communication, sleeping, pausing, restarting, and the SceneManager. + +<quick_start> +Each Scene has its own lifecycle (init, preload, create, update), its own injected systems (`this.add`, `this.input`, `this.cameras`, etc.), and can be started, stopped, paused, slept, or run in parallel with other Scenes. The ScenePlugin (`this.scene`) controls all multi-scene orchestration. + +```js +// Minimal scene with all lifecycle methods +class GameScene extends Phaser.Scene { + constructor() { + super('GameScene'); + } + + init(data) { + // Called first. Receives data passed via scene.start('GameScene', { level: 1 }) + this.level = data.level || 1; + } + + preload() { + // Called after init. Load assets here. + this.load.image('logo', 'assets/logo.png'); + } + + create(data) { + // Called after preload completes. Set up game objects. + // 'data' is the same object passed to init. + this.add.image(400, 300, 'logo'); + } + + update(time, delta) { + // Called every frame while scene is RUNNING. + // time: current time (ms), delta: ms since last frame (smoothed) + } +} + +const config = { + width: 800, + height: 600, + scene: [GameScene] +}; + +const game = new Phaser.Game(config); +``` +</quick_start> + +<core_concepts> +Scene lifecycle states (constants on `Phaser.Scenes`): PENDING (0, registered but not started) -> INIT (1, `scene.init(data)`) -> START (2, `Systems.start()`, emits `start` and `ready`) -> LOADING (3, `scene.preload()` + loader runs) -> CREATING (4, `scene.create(data)`, same `data` as init) -> RUNNING (5, `scene.update(time, delta)` each frame, renders) -> PAUSED (6, no update but still renders) -> SLEEPING (7, no update, no render, state preserved) -> SHUTDOWN (8, emits `shutdown`, can restart) -> DESTROYED (9, fully destroyed, cannot restart). + +Flow with no preload: `init()` -> `create()` -> `update()` loop. Flow with preload: `init()` -> `preload()` -> loader runs -> `create()` -> `update()` loop. + +Scene-injected properties come via the InjectionMap. Global managers (shared across all scenes): `this.game`, `this.renderer`, `this.anims`, `this.cache`, `this.plugins`, `this.registry` (shared data manager), `this.scale`, `this.sound`, `this.textures`. Scene-specific systems (unique per scene): `this.sys` (never overwrite), `this.events` (scene event emitter), `this.cameras`, `this.add` (factory: creates and adds to display list), `this.make` (creator: creates but does NOT add), `this.scene` (ScenePlugin), `this.children` (display list), `this.lights`, `this.data` (scene data manager), `this.input`, `this.load`, `this.time`, `this.tweens`, `this.physics` (if configured), `this.matter` (if configured). + +Rename injected properties via scene config: +```js +const config = { + key: 'MyScene', + map: { + add: 'makeStuff', // this.makeStuff instead of this.add + load: 'loader' // this.loader instead of this.load + } +}; +``` + +The `SceneManager` is a game-level system. Do not call its methods directly -- use `this.scene` (the ScenePlugin) instead. It maintains an ordered array of scenes (determines render/update order) and processes a queue of operations at the start of each game step. All ScenePlugin methods are queued, not immediate -- they execute next frame. +</core_concepts> + +<common_patterns> +Switching between scenes: +```js +// start() -- shuts down current scene, starts target (target gets full lifecycle) +this.scene.start('LevelTwo', { score: 100 }); + +// restart() -- shuts down and restarts the same scene +this.scene.restart({ score: 0 }); + +// switch() -- sleeps current scene, starts/wakes target (current state preserved) +this.scene.switch('PauseMenu', { fromScene: 'GameScene' }); + +// transition() -- animated transition with duration +this.scene.transition({ + target: 'LevelTwo', + duration: 1000, + moveAbove: true, // render target above this scene + sleep: false, // false = stop this scene (default), true = sleep it + remove: false, // true = remove this scene from manager after transition + allowInput: false, // allow input on this scene during transition + data: { score: 100 }, + onUpdate: function (progress) { /* progress: 0 to 1 over duration */ } +}); +``` + +Running scenes in parallel: +```js +// launch() -- starts another scene in parallel (does NOT stop current scene) +this.scene.launch('UIScene', { lives: 3 }); + +// run() -- smart launcher: starts if not running, resumes if paused, wakes if sleeping +this.scene.run('UIScene', { lives: 3 }); + +// Control render order of parallel scenes +this.scene.bringToTop('UIScene'); // render last (on top) +this.scene.sendToBack('Background'); // render first (behind) +this.scene.moveAbove('GameScene', 'UIScene'); // UIScene renders above GameScene +this.scene.moveBelow('GameScene', 'Background'); +this.scene.moveUp('UIScene'); +this.scene.moveDown('UIScene'); +this.scene.swapPosition('SceneA', 'SceneB'); +``` + +Passing data between scenes: +```js +// Method 1: Pass data via start/launch/restart/switch/wake/run -- available in both init(data) and create(data) +this.scene.start('LevelScene', { level: 5, score: 1200 }); + +// Method 2: Access data later via sys.getData() +const data = this.sys.getData(); // returns settings.data + +// Method 3: Global registry (shared across ALL scenes) +this.registry.set('playerHP', 100); +const hp = this.registry.get('playerHP'); // 100, from any scene + +// Method 4: Scene-specific data manager +this.data.set('localValue', 42); +this.data.get('localValue'); // 42 + +// Method 5: Direct scene reference +const otherScene = this.scene.get('OtherScene'); +otherScene.somePublicProperty; + +// Method 6: Events on the global registry +this.registry.events.on('changedata-playerHP', (parent, value, previousValue) => { /* react */ }); +this.registry.set('playerHP', 50); // triggers the event in any listening scene +``` + +Pausing, sleeping, stopping: +```js +this.scene.pause(); // stops update loop, still renders +this.scene.resume('OtherScene', { message: 'welcome back' }); +this.scene.sleep(); // no update AND no render, state preserved +this.scene.wake('OtherScene', { data: 'here' }); +this.scene.stop(); // full shutdown, clears display list and timers + +// State checks +this.scene.isActive('OtherScene'); +this.scene.isPaused('OtherScene'); +this.scene.isSleeping('OtherScene'); +this.scene.isVisible('OtherScene'); + +// Control visibility/activity independently +this.scene.setActive(false); // pause +this.scene.setActive(true); // resume +this.scene.setVisible(false); // hide but still update +this.scene.setVisible(true); +``` + +Adding and removing scenes at runtime: +```js +// add(key, sceneConfig, autoStart, data) +this.scene.add('BonusLevel', BonusLevelScene, false, { someData: true }); +this.scene.remove('BonusLevel'); // destroyed, cannot be restarted + +// Spawn multiple instances from one class +for (let i = 0; i < 5; i++) { + this.scene.add('Level' + i, new LevelScene('Level' + i), false); +} +``` + +Cross-scene event communication -- one scene emits, another listens, decoupled. (In Base44 games, score/HUD readouts belong in the React overlay via EventBus — see the screen_space_ui rules; use this pattern when a second Phaser scene genuinely needs to react to gameplay, e.g. a background or effects layer. The UIScene below illustrates the mechanics.) +```js +// GameScene emits events +class GameScene extends Phaser.Scene { + collectCoin(coin) { + coin.destroy(); + this.events.emit('addScore', 10); + } +} + +// UIScene listens (launched in parallel with { active: true }) +class UIScene extends Phaser.Scene { + constructor() { + super({ key: 'UIScene', active: true }); + } + + create() { + this.score = 0; + this.scoreText = this.add.text(10, 10, 'Score: 0'); + const gameScene = this.scene.get('GameScene'); + gameScene.events.on('addScore', (points) => { + this.score += points; + this.scoreText.setText('Score: ' + this.score); + }); + } +} +``` + +Configuring plugins per scene: +```js +super({ key: 'MinimalScene', plugins: [] }); // disable all default plugins (no load/tweens/time/input/data/lights) +super({ key: 'PreloadScene', plugins: ['Loader'] }); // only this.load available +``` + +Scene config in constructor -- per-scene physics, loader path, or files preloaded before `preload()`: +```js +class Level1 extends Phaser.Scene { + constructor() { + super({ + key: 'Level1', + physics: { arcade: { debug: true, gravity: { y: 200 } } }, + loader: { path: 'assets/levels/1/' }, + // 'pack' loads files before preload() runs -- good for progress bar assets + pack: { files: [{ type: 'image', key: 'bar', url: 'loaderBar.png' }] } + }); + } +} +``` + +Safely restarting scenes -- reset state in `init()`, not the constructor. The constructor runs once; `init()` runs every start: +```js +class GameScene extends Phaser.Scene { + constructor() { + super('GameScene'); + // BAD: this.gameOver = false; -- only set once, not on restart + } + + init() { + // GOOD: reset state every time the scene starts + this.gameOver = false; + this.score = 0; + } + + create() { + // Clean up on shutdown to avoid stale references + this.events.once('shutdown', () => { + this.enemies = []; + }); + } +} +``` +</common_patterns> + +<events> +All events are emitted on `this.events` (the scene-specific EventEmitter). Listen via `this.events.on('eventname', callback)`. + +Lifecycle events: `'boot'` (sys; once, when scene first instantiated), `'start'` (sys; systems start), `'ready'` (sys, data; after start, for user code), `'create'` (scene; after `create()` runs, scene now RUNNING), `'preupdate'` / `'update'` / `'postupdate'` (time, delta; each frame), `'prerender'` / `'render'` (renderer; around scene render). + +State-change events: `'pause'` (sys, data), `'resume'` (sys, data), `'sleep'` (sys, data), `'wake'` (sys, data), `'shutdown'` (sys, data), `'destroy'` (sys). + +Transition events: `'transitionout'` (targetScene, duration; on source scene), `'transitioninit'` (fromScene, duration; on target during init), `'transitionstart'` (fromScene, duration; on target after create), `'transitionwake'` (fromScene, duration; on target if woken from sleep), `'transitioncomplete'` (scene; on target when done). + +Game object events: `'addedtoscene'` (gameObject, scene), `'removedfromscene'` (gameObject, scene). +</events> + +<gotchas> +- **Operations are queued, not immediate.** `this.scene.start('X')` happens at the next SceneManager update, not synchronously. Do not rely on the target scene's state within the same frame. +- **`start()` shuts down the calling scene.** It stops the current scene and starts X. To keep both running, use `launch()` or `run()`. +- **`switch()` sleeps, `start()` shuts down.** `switch()` preserves the current scene in memory (sleep); `start()` triggers a full shutdown. Sleeping scenes keep their events and references live. +- **`switch()` restarts a paused scene, never resumes it.** If you need resume behavior, use `run()` instead. +- **Paused scenes still render.** `pause()` only stops the update loop. Use `sleep()` to stop both update and render. +- **Do not overwrite `this.sys`.** It will break everything. +- **`this.scene.start()` with no key restarts the current scene** -- equivalent to `this.scene.restart()`. +- **Sleeping scenes still receive events from other scenes.** If Scene A is sleeping but Scene B emits on the global registry, Scene A's listeners still fire. Be careful with active listeners on sleeping scenes. +- **Scene render order = array order.** Scenes later in the array render on top. Use `bringToTop()`, `sendToBack()`, `moveAbove()`, `moveBelow()` to control layering. Scenes update in reverse order (top updates first, gets input priority) but render in forward order (appears on top). +- **`shutdown` vs `destroy`.** Shutdown puts a scene into hibernation (can restart); destroy permanently removes it. Listen to `'shutdown'` to free resources recreated on restart, `'destroy'` for final cleanup. +- **`this.physics` and `this.matter` are only available if the physics system is configured** -- otherwise undefined. +- **The `create` event fires AFTER the `create()` method returns**, and after the status changes to RUNNING. Listen for it if you need post-create setup. +- **Reset state in `init()`, not the constructor.** The constructor runs once; `init()` runs every start/restart. +- **Clean up on `shutdown`** via `this.events.once('shutdown', ...)` to clear arrays holding game objects and remove external listeners. Stale references to destroyed objects cause errors on restart. +</gotchas> +</scenes> diff --git a/skills/base44-platform-games/references/phaser/sprites_and_images.md b/skills/base44-platform-games/references/phaser/sprites_and_images.md new file mode 100644 index 0000000..706ea10 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/sprites_and_images.md @@ -0,0 +1,207 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: sprites and images + +<sprites_and_images> +Use this when creating or manipulating Sprite and Image game objects in Phaser 4: factory methods, texture/frame selection, the component mixin system, and visual operations (position, scale, rotation, tint, flip, alpha, origin, depth). + +<quick_start> +```js +// In a Scene's create() method: + +// Static image (no animation support, slightly cheaper) +const bg = this.add.image(400, 300, 'background'); + +// Sprite (supports animations) +const player = this.add.sprite(100, 200, 'player', 'idle-0'); + +// Common operations -- all methods return `this` for chaining +player.setPosition(200, 300); +player.setScale(2); +player.setAngle(45); +player.setTint(0xff0000); +player.setAlpha(0.8); +player.setOrigin(0, 1); // bottom-left +player.setDepth(10); +player.setFlip(true, false); // flip horizontally +player.setVisible(false); + +// Chained +this.add.sprite(100, 100, 'coin') + .setScale(0.5) + .setTint(0xffff00) + .play('spin'); +``` +</quick_start> + +<core_concepts> +**Sprite vs Image.** Both extend `GameObject` and share the same set of component mixins. The only difference: Sprite includes an `AnimationState` instance (`sprite.anims`) and animation methods (`play`, `stop`, `chain`). Image has no animation support, no per-frame `preUpdate`, and is not added to the Scene `updateList`. Both support static texture display, tint/alpha/flip/scale/rotate, physics bodies, and input/hit areas. + +Rule of thumb: use `Image` for anything that does not need frame-by-frame animation -- it skips the per-frame `preUpdate` cost and has a smaller API surface. Use `Sprite` only when you need the Animation component. + +**The component mixin system.** Phaser builds Game Object classes by mixing component objects into the prototype. Both Sprite and Image share an identical Mixins array: Alpha, BlendMode, Depth, Flip, GetBounds, Lighting, Mask, Origin, RenderNodes, ScrollFactor, Size, TextureCrop, Tint, Transform, Visible, plus a render-specific mixin (SpriteRender / ImageRender). The base `GameObject` itself mixes in Filters and RenderSteps. When you see a method like `setAlpha()` on a Sprite, it comes from `Components.Alpha`, not the Sprite class itself. + +**Texture and frame** come from the `TextureCrop` component: `texture` (the Texture instance), `frame` (current Frame instance), `setTexture(key, frame)`, `setFrame(frame, updateSize, updateOrigin)`, `setCrop(x, y, width, height)`, and `isCropped`. The `texture` param accepts a string key or a `Phaser.Textures.Texture` instance. The `frame` param accepts a string name or numeric index; if omitted, the base frame (`'__BASE'`) is used. + +```js +// Change texture at runtime +sprite.setTexture('enemies', 'goblin-walk-1'); + +// Change only the frame (must belong to current texture) +sprite.setFrame('goblin-walk-2'); + +// setFrame(frame, updateSize=true, updateOrigin=true) -- pass false to skip resize/origin recalc +sprite.setFrame('small-frame', false, false); + +// Crop to a 50x50 region starting at (10, 10); call setCrop() with no args to reset +sprite.setCrop(10, 10, 50, 50); +sprite.setCrop(); +``` +</core_concepts> + +<common_patterns> +Position and size (Transform + Size components): +```js +sprite.x, sprite.y // position (read/write; default 0) +sprite.z // z position (does NOT control render order -- use depth) +sprite.setPosition(x, y, z, w) // y defaults to x if omitted +sprite.setRandomPosition(x, y, w, h) // random within area; defaults to game size +sprite.copyPosition(source) // copy from any {x, y} object + +sprite.width, sprite.height // native (un-scaled) size +sprite.displayWidth, sprite.displayHeight // scaled size (read/write -- setting adjusts scale) +sprite.setSize(width, height) // internal size (not visual) +sprite.setDisplaySize(width, height) // visual size (adjusts scale) +sprite.setSizeToFrame(frame) // reset size to match frame +``` + +Scaling and rotation (Transform): +```js +sprite.scale // uniform scale (getter returns average of scaleX+scaleY) +sprite.scaleX, sprite.scaleY // default 1 +sprite.setScale(x, y) // y defaults to x if omitted + +sprite.rotation // radians (clockwise: 0=right, PI/2=down) +sprite.angle // degrees (0=right, 90=down, 180/-180=left, -90=up) +sprite.setRotation(radians) +sprite.setAngle(degrees) +``` + +Tinting (Tint component, WebGL only): +```js +sprite.tint // getter returns tintTopLeft +sprite.tintTopLeft / tintTopRight / tintBottomLeft / tintBottomRight // default 0xffffff +sprite.tintMode // default Phaser.TintModes.MULTIPLY +sprite.isTinted // read-only + +sprite.setTint(topLeft, topRight, bottomLeft, bottomRight) // single arg applies to all four corners +sprite.setTintMode(mode) // MULTIPLY | FILL | ADD | SCREEN | OVERLAY | HARD_LIGHT +sprite.clearTint() // resets to 0xffffff + MULTIPLY +``` +Phaser 4 change: `setTintFill()` is removed -- use `setTint(color).setTintMode(Phaser.TintModes.FILL)`. + +Alpha (Alpha component): +```js +sprite.alpha // global alpha 0-1 (default 1) +sprite.alphaTopLeft / alphaTopRight / alphaBottomLeft / alphaBottomRight // per-corner (WebGL only) +sprite.setAlpha(topLeft, topRight, bottomLeft, bottomRight) // single arg applies to whole object +sprite.clearAlpha() // resets to 1 (fully opaque) +``` +Setting `alpha` to 0 clears the render flag so the object is not drawn; any non-zero value restores it. + +Flipping (Flip component) -- a rendering toggle only; does not affect physics bodies, hit areas, or scale value: +```js +sprite.flipX, sprite.flipY // boolean (default false) +sprite.setFlipX(value) +sprite.setFlipY(value) +sprite.setFlip(x, y) +sprite.toggleFlipX() +sprite.toggleFlipY() +sprite.resetFlip() // both to false +``` + +Origin and depth (Origin + Depth components): +```js +sprite.originX, sprite.originY // normalized 0-1 (default 0.5, center) +sprite.displayOriginX, sprite.displayOriginY // pixel value (originX * width) +sprite.setOrigin(x, y) // y defaults to x; defaults to 0.5 +sprite.setDisplayOrigin(x, y) // set origin in pixels +sprite.setOriginFromFrame() // use Frame pivot if set, else 0.5 + +sprite.depth // default 0 (higher = renders on top) +sprite.setDepth(value) // queues a depth sort +sprite.setToTop() // move to top of display list (no depth change) +sprite.setToBack() +sprite.setAbove(gameObject) +sprite.setBelow(gameObject) +``` +`setToTop`/`setToBack`/`setAbove`/`setBelow` change display list position without modifying the `depth` value. + +Additional components: +```js +sprite.visible, sprite.setVisible(value) // Visible -- invisible objects skip render but still update +sprite.scrollFactorX, sprite.scrollFactorY // ScrollFactor (default 1: moves with camera) +sprite.setScrollFactor(x, y) // 0 = fixed to camera; y defaults to x +sprite.blendMode, sprite.setBlendMode(value) // BlendMode (NORMAL, ADD, MULTIPLY, SCREEN, ERASE) +``` +Performance: changing blend mode breaks WebGL batches. Group objects by blend mode when possible to minimize draw calls. + +Destroying: +```js +sprite.destroy(); // removes from display + update lists, cleans up +sprite.ignoreDestroy = true; // prevent destruction (you manage lifecycle) +sprite.isDestroyed; // boolean, true after destroy() +``` +For Sprites, `destroy()` also calls `this.anims.destroy()` to clean up the AnimationState. +</common_patterns> + +<factory_methods> +```js +// this.add.sprite(x, y, texture, frame) -> Phaser.GameObjects.Sprite +this.add.sprite(x, y, texture, frame); + +// this.add.image(x, y, texture, frame) -> Phaser.GameObjects.Image +this.add.image(x, y, texture, frame); +``` +`x`, `y` are world coordinates (required). `texture` is a string key or Texture instance (required). `frame` is a string name or numeric index (optional). Both constructors run the same init sequence: `setTexture` -> `setPosition` -> `setSizeToFrame` -> `setOriginFromFrame` -> `initRenderNodes`. The factory does `this.displayList.add(new Sprite/Image(...))`. +</factory_methods> + +<specialized_game_objects> +**NineSlice** -- scalable UI panels with fixed-size corners and stretchable edges/center. Ideal for buttons, panels, dialog boxes that resize without distorting borders: +```js +// nineslice(x, y, texture, frame, width, height, leftWidth, rightWidth, topHeight, bottomHeight) +const panel = this.add.nineslice(400, 300, 'panel', null, 300, 200, 20, 20, 20, 20); +panel.setSize(500, 400); // corners stay fixed, edges stretch +``` + +**TileSprite** -- repeating texture that fills an area. Supports atlas frames and a `tileRotation` property in v4: +```js +const bg = this.add.tileSprite(400, 300, 800, 600, 'grass'); +// Scroll in update() for parallax backgrounds +bg.tilePositionX += 0.5; +bg.tilePositionY += 0.25; +bg.tileRotation = 0.1; // v4: rotate the tile pattern +``` + +**Blitter** -- high-performance batched rendering for large numbers of static/semi-static sprites. Children (Bobs) have no rotation, scale, or physics, but render significantly faster: +```js +const blitter = this.add.blitter(0, 0, 'particles'); +for (let i = 0; i < 1000; i++) { + const bob = blitter.create( + Phaser.Math.Between(0, 800), + Phaser.Math.Between(0, 600), + 'particle_01' // frame name within the texture + ); + bob.setAlpha(Math.random()); +} +``` + +**Video** -- video playback as a game object with position, scale, and filter support: +```js +this.load.video('intro', 'intro.mp4'); // in preload +const video = this.add.video(400, 300, 'intro'); // in create +video.play(); +``` +</specialized_game_objects> +</sprites_and_images> diff --git a/skills/base44-platform-games/references/phaser/text_and_bitmaptext.md b/skills/base44-platform-games/references/phaser/text_and_bitmaptext.md new file mode 100644 index 0000000..965c3d7 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/text_and_bitmaptext.md @@ -0,0 +1,249 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: text and bitmaptext + +<text_and_bitmaptext> +Use this when displaying text in Phaser 4: the Canvas-based Text game object, TextStyle configuration, word wrap, BitmapText (static and dynamic), retro fonts, alignment, text bounds, and padding. + +In Base44 games this section applies to IN-WORLD text: damage numbers, floating labels, speech bubbles anchored to characters, per-frame counters rendered inside the world. Screen-space UI text — menus, score/lives HUD, game-over screens — is built in the React overlay instead (see the screen_space_ui rules in react_phaser_architecture). + +<quick_start> +```js +// Canvas-based Text (flexible styling, uses browser fonts) +const title = this.add.text(400, 50, 'Hello World', { + fontFamily: 'Arial', + fontSize: '32px', + color: '#ffffff' +}); + +// BitmapText (pre-rendered font texture, faster rendering) +// Font must be loaded first: this.load.bitmapFont('myFont', 'font.png', 'font.xml') +const score = this.add.bitmapText(400, 100, 'myFont', 'Score: 0', 32); + +// DynamicBitmapText (per-character manipulation via callback) +const fancy = this.add.dynamicBitmapText(400, 200, 'myFont', 'Wavy!', 32); +fancy.setDisplayCallback(function (data) { + data.y += Math.sin(data.index * 0.5 + fancy.scene.time.now * 0.005) * 10; + return data; +}); +``` +</quick_start> + +<core_concepts> +**Text vs BitmapText.** `Text` renders via the Canvas 2D API (uploaded as a texture); supports web/CSS fonts, shadows, gradients, strokes, built-in word wrap, justify alignment, and RTL; slower (re-creates the canvas texture on every change). `BitmapText` uses a pre-rendered font texture; no web fonts; drop shadow only (WebGL); `setMaxWidth` wrap; per-character/word tinting (WebGL); fast batched rendering; alignment via `ALIGN_LEFT` (0), `ALIGN_CENTER` (1), `ALIGN_RIGHT` (2). `DynamicBitmapText` is the same as BitmapText plus a per-character `setDisplayCallback` for animation effects (moderate cost). + +Rule of thumb: use `Text` for infrequently-updated in-world text needing flexible styling or web fonts. Use `BitmapText` for in-world text that updates frequently or is rendered in large quantities (damage numbers, combo counters). Use `DynamicBitmapText` only when you need per-character animation. + +**TextStyle.** The `Text` object delegates all styling to its `TextStyle` instance (`text.style`). Pass a style config when creating the Text, or update it later with setter methods; the style object uses Canvas 2D properties internally. Defaults: `fontFamily` `'Courier'`, `fontSize` `'16px'`, `color` `'#fff'`. The `font` shorthand (e.g. `'bold 24px Arial'`) overrides `fontFamily`, `fontSize`, and `fontStyle` when provided. Other notable style properties: `stroke` + `strokeThickness`, `backgroundColor`, `align` (`left`/`right`/`center`/`justify`), `maxLines`, `fixedWidth` / `fixedHeight`, `resolution`, `rtl`, `wordWrap` (`{ width, callback, callbackScope, useAdvancedWrap }`), `shadow` (`{ offsetX, offsetY, color, blur, stroke, fill }`), `padding`, `lineSpacing`, `letterSpacing`, `metrics` (pre-computed `{ ascent, descent, fontSize }` to skip measurement). +</core_concepts> + +<common_patterns> +Basic Text creation: +```js +const label = this.add.text(100, 100, 'Player 1', { + fontFamily: 'Georgia', fontSize: '24px', color: '#00ff00' +}); + +// font shorthand (sets fontStyle, fontSize, fontFamily) +const bold = this.add.text(100, 150, 'Bold Text', { font: 'bold 28px Arial' }); + +// Array of strings creates multi-line text +const multi = this.add.text(100, 200, ['Line 1', 'Line 2', 'Line 3'], { + fontFamily: 'Arial', fontSize: '18px', color: '#ffffff' +}); + +// Text origin defaults to (0, 0) -- top-left. Center it: +label.setOrigin(0.5); +``` + +Styling Text (all setters return `this`): +```js +const text = this.add.text(400, 300, 'Styled', { + fontFamily: 'Verdana', fontSize: '48px', color: '#ff0000', + stroke: '#000000', strokeThickness: 4, backgroundColor: '#333333', + shadow: { offsetX: 2, offsetY: 2, color: '#000', blur: 4, stroke: false, fill: true } +}); + +text.setColor('#00ffff'); +text.setFontSize(64); +text.setFontFamily('Courier'); +text.setFontStyle('italic'); +text.setStroke('#ff00ff', 6); +text.setShadow(3, 3, '#000', 5, false, true); +text.setBackgroundColor('#222'); +text.setStyle({ fontSize: '64px', fontFamily: 'Arial', color: '#ffffff', align: 'center' }); // replace all + +// Canvas gradients and patterns work as fill/stroke +const gradient = text.context.createLinearGradient(0, 0, 0, text.height); +gradient.addColorStop(0, '#ff0000'); +gradient.addColorStop(1, '#0000ff'); +text.setFill(gradient); +``` + +Word wrap and alignment: +```js +const wrapped = this.add.text(50, 50, 'Long text here...', { + fontFamily: 'Arial', fontSize: '20px', color: '#fff', wordWrap: { width: 300 } +}); +wrapped.setWordWrapWidth(300); +wrapped.setWordWrapWidth(300, true); // advanced: collapses spaces, trims whitespace +wrapped.setWordWrapCallback(function (text, textObject) { + return text.split(' ').join('\n'); // return string with \n or array of lines +}); +const lines = wrapped.getWrappedText(); + +// Alignment only affects multi-line text: 'left' (default), 'right', 'center', 'justify' +const aligned = this.add.text(400, 100, 'Line 1\nLonger Line 2\nLine 3', { + fontFamily: 'Arial', fontSize: '24px', color: '#fff', align: 'center' +}); +aligned.setAlign('right'); +// For center alignment to look correct, combine with a fixed width: +aligned.setFixedSize(300, 0); +aligned.setAlign('center'); +``` + +Dynamic updates, padding, fixed size, spacing: +```js +const comboText = this.add.text(10, 10, 'Combo: 0', { fontFamily: 'Arial', fontSize: '24px', color: '#fff' }); +comboText.setText('Combo: 12'); +comboText.setText(['Combo: 12', 'Best: 30']); // joins with \n +comboText.appendText('Extra line'); // prepends \n by default +comboText.appendText(' more', false); // no carriage return +const current = comboText.text; + +// Padding adds space around text inside the canvas +const padded = this.add.text(100, 100, 'Padded', { + fontFamily: 'Arial', fontSize: '24px', color: '#fff', backgroundColor: '#333', + padding: { left: 10, right: 10, top: 5, bottom: 5 } +}); +padded.setPadding({ x: 10, y: 5 }); // shorthand: x=left+right, y=top+bottom +padded.setPadding(10, 5, 10, 5); // left, top, right, bottom +padded.setPadding(10); // single value = all sides + +// Fixed dimensions create a text canvas of exact size +const fixed = this.add.text(100, 100, 'Fixed box', { + fontFamily: 'Arial', fontSize: '18px', color: '#fff', + fixedWidth: 200, fixedHeight: 100, wordWrap: { width: 200 }, align: 'center' +}); +fixed.setMaxLines(3); + +// Line and letter spacing +const spaced = this.add.text(100, 200, 'Spaced\nLines', { + fontFamily: 'Arial', fontSize: '24px', color: '#fff', lineSpacing: 10, letterSpacing: 2 +}); +spaced.setLineSpacing(20); +spaced.setLetterSpacing(5); +``` + +BitmapText creation, alignment, shadow, tinting: +```js +// Load in preload: this.load.bitmapFont('pixelFont', 'font.png', 'font.xml'); +const bmpText = this.add.bitmapText(100, 100, 'pixelFont', 'Hello', 32); + +// align: 0 = left, 1 = center, 2 = right +const aligned = this.add.bitmapText(100, 200, 'pixelFont', 'Line 1\nLine 2', 24, 1); +bmpText.setLeftAlign(); +bmpText.setCenterAlign(); +bmpText.setRightAlign(); + +bmpText.setText('Updated!'); // or: bmpText.text = 'Also works'; +bmpText.setFontSize(48); // or: bmpText.fontSize = 48; +bmpText.setLetterSpacing(2); +bmpText.setLineSpacing(5); +bmpText.setMaxWidth(200); // word wrap by max pixel width + +// Drop shadow (WebGL only, static BitmapText only); call with no args to clear +bmpText.setDropShadow(2, 2, 0x000000, 0.5); +bmpText.setDropShadow(); + +// Tint characters/words (WebGL only) +// setCharacterTint(start, length, tintMode, color) -- length -1 = all from start +bmpText.setCharacterTint(0, 5, Phaser.TintModes.MULTIPLY, 0xff0000); +bmpText.setWordTint('Score', -1, Phaser.TintModes.MULTIPLY, 0x00ff00); // by string match or word index +``` + +DynamicBitmapText: +```js +const dynamic = this.add.dynamicBitmapText(100, 100, 'pixelFont', 'Dynamic!', 32); + +// Per-character display callback -- invoked each render frame per character +// data: { parent, tint, index, charCode, x, y, scale, rotation, data } +dynamic.setDisplayCallback(function (data) { + data.x += Math.sin(data.index + dynamic.scene.time.now * 0.01) * 5; + data.y += Math.cos(data.index + dynamic.scene.time.now * 0.01) * 5; + return data; +}); + +// Scrolling text window +dynamic.setSize(200, 50); // crop region in pixels +dynamic.setScrollX(10); +dynamic.setScrollY(0); +``` + +Retro fonts use a fixed-width character grid from a standard image (no XML/JSON font file): +```js +this.load.image('retroFont', 'retroFont.png'); // in preload + +const config = { + image: 'retroFont', + width: 8, height: 8, // character size in pixels + chars: Phaser.GameObjects.RetroFont.TEXT_SET1, + charsPerRow: 16, + offset: { x: 0, y: 0 }, spacing: { x: 0, y: 0 }, lineSpacing: 0 +}; +this.cache.bitmapFont.add('retroFont', { data: Phaser.GameObjects.RetroFont.Parse(this, config) }); +const retro = this.add.bitmapText(100, 100, 'retroFont', 'RETRO TEXT', 8); // use like any BitmapText +``` +Available `Phaser.GameObjects.RetroFont` TEXT_SET constants: `TEXT_SET1` (full ASCII printable), `TEXT_SET2` (space through Z), `TEXT_SET3`-`TEXT_SET11` (subsets of uppercase, digits, punctuation). `TEXT_SET10` is uppercase letters only. + +Text bounds (BitmapText) and high-DPI resolution: +```js +const bounds = bmpText.getTextBounds(); +// bounds.local -- { x, y, width, height } at origin 0,0 +// bounds.global -- with scale + world position +// bounds.lines -- per-line rects +// bounds.words -- word objects with positions +// bounds.characters -- character objects with positions +console.log(bmpText.width, bmpText.height); // read-only, computed from global bounds +const char = bmpText.getCharacterAt(pointer.worldX, pointer.worldY); // for click detection + +// Higher resolution for crisp text on HiDPI displays (costs more memory) +const crisp = this.add.text(100, 100, 'Sharp!', { + fontFamily: 'Arial', fontSize: '24px', color: '#fff', resolution: 2 +}); +crisp.setResolution(window.devicePixelRatio); +``` +</common_patterns> + +<factory_methods> +```js +// Text -- Canvas-based. text: string | string[]; style: optional. Returns Phaser.GameObjects.Text +this.add.text(x, y, text, style); + +// BitmapText -- static, fast. font: cache key; text: optional; size: optional (defaults to font data size); align: optional (0). Returns Phaser.GameObjects.BitmapText +this.add.bitmapText(x, y, font, text, size, align); + +// DynamicBitmapText -- per-character callback. Same args as bitmapText. Returns Phaser.GameObjects.DynamicBitmapText +this.add.dynamicBitmapText(x, y, font, text, size); +``` +BitmapText alignment constants: `BitmapText.ALIGN_LEFT` (0), `BitmapText.ALIGN_CENTER` (1), `BitmapText.ALIGN_RIGHT` (2). +</factory_methods> + +<gotchas> +- **Text re-renders on every change.** Each `setText`/`setStyle`/`setColor` re-creates the internal canvas and (in WebGL) re-uploads the texture. Batch style changes -- set the style object properties directly and call `updateText()` once at the end. +- **Font must be loaded before use.** Text uses the Canvas `fillText` API, so fonts must be available in the browser (CSS, a web font loader, or already installed). Phaser does not load web fonts. +- **Font names with special characters need quotes.** Names with digits or special characters must be double-quoted: `fontFamily: '"Press Start 2P"'`. +- **Text origin defaults to (0, 0)** (top-left), unlike Sprites (0.5, 0.5). Call `setOrigin(0.5)` to center. +- **Alignment only affects multi-line text.** `align: 'center'` does nothing on a single line -- use `setOrigin(0.5)` or manual positioning. +- **BitmapText font must be in cache.** The key passed to `this.add.bitmapText()` must match a key loaded via `this.load.bitmapFont()`, else a console warning and undefined behavior. +- **BitmapText `setMaxWidth` only wraps on whitespace.** If no whitespace (default char code 32) is found, no wrapping occurs. Change the wrap character with the second argument. +- **DynamicBitmapText is more expensive** -- the display callback runs for every character every frame. Use only when you need per-character manipulation. +- **`setCharacterTint` and `setWordTint` are WebGL only** -- no effect in Canvas renderer. +- **`setDropShadow` is WebGL only and static BitmapText only** -- not DynamicBitmapText or Canvas renderer. +- **`letterSpacing` on Text is expensive** -- non-zero spacing renders each character individually with `fillText`. Use BitmapText for large amounts of letter-spaced text. +- **`setFill` and `setColor` are equivalent** -- both set the `color` property; the style config also accepts `fill` as an alias. +- **`setRTL` must be called BEFORE `setText`.** RTL mode changes how the internal canvas is constructed. Set it in the style config or call `setRTL()` before any `setText()`. +- **BitmapText is much faster for frequently updating content.** Text re-renders the whole canvas and re-uploads the GPU texture on every change; BitmapText just updates vertex data. For damage numbers, combo counters, or any per-frame in-world text, always prefer BitmapText. (Score/timer HUD readouts belong in the React overlay, not Phaser text.) +</gotchas> +</text_and_bitmaptext> diff --git a/skills/base44-platform-games/references/phaser/tilemaps.md b/skills/base44-platform-games/references/phaser/tilemaps.md new file mode 100644 index 0000000..2087a8a --- /dev/null +++ b/skills/base44-platform-games/references/phaser/tilemaps.md @@ -0,0 +1,218 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: tilemaps + +<tilemaps> +Use this when working with tilemaps in Phaser 4: loading Tiled JSON maps, creating tilemap layers, tile collision, dynamic tiles, tile properties, object layers, and orthogonal/isometric/hexagonal/staggered maps. + +<quick_start> +A `Tilemap` holds parsed map data (layers, tilesets, objects) and provides methods to add tilesets, create layers, set collision, and query tiles. Layers (`TilemapLayer` or `TilemapGPULayer`) are the Game Objects that actually render tiles. + +```js +class GameScene extends Phaser.Scene { + preload() { + // Load the Tiled JSON and the tileset image + this.load.tilemapTiledJSON('map', 'assets/level1.json'); + this.load.image('tiles', 'assets/tilesheet.png'); + } + + create() { + // Create the tilemap from cached JSON + const map = this.add.tilemap('map'); + + // Link the tileset image to the tileset name used in Tiled + const tileset = map.addTilesetImage('tilesheet', 'tiles'); + + // Create a layer - layerID must match the layer name in Tiled + const ground = map.createLayer('Ground', tileset); + + // Enable collision on specific tile indexes + ground.setCollision([1, 2, 3]); + } +} +``` + +The flow is always: load JSON + image, create tilemap, add tileset image, create layer(s), set collision. +</quick_start> + +<core_concepts> +**Tilemap vs Layer.** A `Tilemap` is a data container, not a display object -- it stores parsed map data and provides methods that operate on it. A `TilemapLayer` or `TilemapGPULayer` is the actual Game Object added to the display list that renders tiles. `this.add.tilemap(key)` is a factory on `GameObjectFactory` that delegates to `ParseToTilemap`, reading from the cache and returning a `Tilemap`. + +```js +const map = this.add.tilemap('map'); // Data container (not rendered) +const layer = map.createLayer('Ground', tileset); // Game Object (rendered) +``` + +**Tilesets.** A `Tileset` links a tileset name (from Tiled) to a loaded texture; it stores `firstgid`, tile dimensions, margin, and spacing. +```js +// tilesetName: the name in Tiled's tileset panel; key: the Phaser texture key (defaults to tilesetName) +const tileset = map.addTilesetImage('tilesetName', 'textureKey'); +// Override tile dimensions, margin, and spacing if needed +const tileset = map.addTilesetImage('name', 'key', 16, 16, 1, 2); +``` +`addTilesetImage(tilesetName, key, tileWidth, tileHeight, tileMargin, tileSpacing, gid, tileOffset)` -- if the tileset name already exists in the parsed map data, it updates the existing Tileset with the texture; otherwise (non-Tiled maps) it creates a new one. The Phaser Tiled parser does NOT support "Collection of Images" tilesets -- all tiles must be in a single tileset image per tileset. + +**The Tile class.** Each cell in a layer is a `Tile`. Key properties: `index` (tile index in the tileset, -1 for empty), `x`/`y` (tile coordinates, not pixels), `pixelX`/`pixelY` (pixel position relative to layer origin), `width`/`height` (pixels), `properties` (custom properties object from Tiled), `collideLeft`/`Right`/`Up`/`Down` (per-edge collision flags), `faceLeft`/`Right`/`Top`/`Bottom` (interesting-face flags for collision optimization), `collisionCallback` (per-tile callback), `tint` (default `0xffffff`), `tintMode` (default `TintModes.MULTIPLY`), `rotation`, `physics` (engine-specific data e.g. bodies), and inherited `alpha`/`visible`/`flipX`/`flipY`. + +**TilemapGPULayer (v4.0.0)** is a high-performance WebGL-only alternative to `TilemapLayer`, rendering the entire layer as a single quad via a shader (almost entirely GPU-bound). Pass `gpu: true` as the 5th arg to `createLayer`: +```js +const layer = map.createLayer('Ground', tileset, 0, 0, true); +``` +Capabilities: single tileset per layer only, max 4096x4096 tiles, max unique tile IDs 2^23, supports tile flip and tile animation, orthographic maps only, smooth borders with LINEAR filtering or sharp pixels with NEAREST. Restrictions: layer edits do NOT display automatically -- call `generateLayerDataTexture()` after modifying tiles; WebGL only (no Canvas fallback); cannot use multiple tilesets. +```js +gpuLayer.putTileAt(5, 10, 10); +gpuLayer.generateLayerDataTexture(); // required after edits +``` + +Both `TilemapLayer` and `TilemapGPULayer` extend `TilemapLayerBase` (which extends `GameObject`). The base class provides all tile query, manipulation, and collision methods, with these mixins: Alpha, BlendMode, ComputedSize, Depth, ElapseTimer, Flip, GetBounds, Lighting, Mask, Origin, RenderNodes, Transform, Visible, ScrollFactor, and Arcade Physics Collision. +</core_concepts> + +<common_patterns> +Creating from Tiled JSON and multiple layers: +```js +const map = this.add.tilemap('map'); +const tileset = map.addTilesetImage('TilesetNameInTiled', 'tiles'); +const layer = map.createLayer('LayerNameInTiled', tileset); + +// Multiple layers render in creation order; use depth for finer control +const background = map.createLayer('Background', tileset); +const ground = map.createLayer('Ground', tileset); +const foreground = map.createLayer('Foreground', tileset); +foreground.setDepth(10); + +// A CPU layer can use multiple tilesets +const tiles1 = map.addTilesetImage('terrain', 'terrain-img'); +const tiles2 = map.addTilesetImage('objects', 'objects-img'); +const multi = map.createLayer('Ground', [tiles1, tiles2]); +``` +The `layerID` passed to `createLayer` must match the layer name in Tiled exactly. Group layer children are flattened with a `'ParentGroup/Layer'` naming convention. + +Blank layer for procedural maps: +```js +// createBlankLayer(name, tileset, x, y, width, height, tileWidth, tileHeight) +const layer = map.createBlankLayer('dynamic', tileset, 0, 0, 50, 50, 32, 32); +layer.fill(1); // fill entire layer with tile index 1 +layer.putTileAt(5, 10, 10); // place tile index 5 at tile coord (10, 10) +``` + +Collision setup (several ways): +```js +layer.setCollision([1, 2, 3]); // by specific indexes +layer.setCollisionBetween(1, 50); // by range (inclusive) +layer.setCollisionByProperty({ collides: true }); // by Tiled property; supports arrays: { type: ['stone', 'lava'] } +layer.setCollisionByExclusion([-1, 0]); // collide on all tiles EXCEPT these (-1 empty, 0 often background) +layer.setCollisionFromCollisionGroup(); // from Tiled collision editor shapes +``` +All collision methods on `TilemapLayerBase` mirror methods on `Tilemap` but without a `layer` param. On the `Tilemap` you can use the "current layer" or specify one explicitly: +```js +map.setLayer('Ground'); +map.setCollision([1, 2, 3]); // applies to current layer +map.setCollision([1, 2, 3], true, true, 'Ground'); // explicit layer +``` + +Physics integration (Arcade) -- the layer must have collision set for physics to detect it: +```js +this.physics.add.collider(player, groundLayer); +this.physics.add.collider(player, groundLayer, (sprite, tile) => { + if (tile.index === 5) { /* hit a special tile */ } +}); +this.physics.add.overlap(player, groundLayer, (sprite, tile) => { /* overlapping */ }); +``` +The layer has `collisionCategory` and `collisionMask` properties for collision filtering. + +Tile properties and callbacks (callbacks require an active physics collider/overlap between body and layer): +```js +const tile = layer.getTileAt(10, 5); +console.log(tile.properties.damage, tile.properties.type); // custom props from Tiled +layer.setCollisionByProperty({ type: ['wall', 'rock'] }); + +// Callback by tile index -- fires when a physics body overlaps these tiles +map.setTileIndexCallback([5, 6, 7], (sprite, tile) => { /* ... */ }, this); +// Callback by tile location -- fires for tiles in a rectangular area +map.setTileLocationCallback(10, 10, 5, 5, (sprite, tile) => { /* ... */ }, this); +// Per-tile callback +layer.getTileAt(10, 5).collisionCallback = (sprite, tile) => { /* ... */ }; +``` + +Querying tiles: +```js +const tile = layer.getTileAt(10, 5); // by tile coords (or null) +const tile = layer.getTileAt(10, 5, true); // nonNull: Tile with index -1 instead of null +const tile = layer.getTileAtWorldXY(worldX, worldY); // by world coords +const exists = layer.hasTileAt(10, 5); + +const tiles = layer.getTilesWithin(0, 0, 10, 10); // tile coord region +const tiles = layer.getTilesWithinWorldXY(x, y, w, h); // world coord region +const tiles = layer.getTilesWithinShape(circle); // shape overlap + +const water = layer.filterTiles(t => t.properties.type === 'water'); +const spawn = layer.findTile(t => t.properties.isSpawn); +layer.forEachTile(t => { /* iterate all tiles */ }); +``` + +Modifying tiles at runtime: +```js +layer.putTileAt(5, 10, 10); // place tile index 5 at (10, 10) +layer.putTileAtWorldXY(5, worldX, worldY); // place by world coords +layer.putTilesAt([[1, 2], [3, 4]], 10, 10); // place a 2x2 grid +layer.removeTileAt(10, 10); +layer.fill(1, 0, 0, 10, 10); // fill 10x10 region with index 1 +layer.replaceByIndex(5, 10); // replace all index-5 with index-10 +layer.copy(0, 0, 5, 5, 20, 20); // copy 5x5 from (0,0) to (20,20) +layer.randomize(0, 0, 10, 10, [1, 2, 3, 4]); +layer.weightedRandomize([{ index: 1, weight: 4 }, { index: 2, weight: 1 }], 0, 0, 10, 10); +layer.shuffle(0, 0, 10, 10); +``` + +Coordinate conversion: +```js +const tileXY = layer.worldToTileXY(worldX, worldY); +const worldXY = layer.tileToWorldXY(tileX, tileY); + +// Reuse a vector to avoid allocation +const vec = new Phaser.Math.Vector2(); +layer.worldToTileXY(worldX, worldY, true, vec); // snapToFloor = true +``` + +Object layers (Tiled) -- use `createFromObjects` to turn points/rectangles/sprite placements into Game Objects: +```js +// Match by tile GID, with a custom class extending Sprite +const enemies = map.createFromObjects('Enemies', { gid: 26, classType: Enemy }); +// Match by name +const coins = map.createFromObjects('Items', { name: 'coin', key: 'coin-texture', frame: 0 }); +// Match by type +const spawns = map.createFromObjects('Spawns', { type: 'player-spawn' }); + +// Access raw object layer data +const objectLayer = map.getObjectLayer('Enemies'); +objectLayer.objects.forEach(obj => console.log(obj.name, obj.x, obj.y, obj.properties)); +``` +`createFromObjects(layerName, config, useTileset)` config options: `id`, `gid`, `name`, `type`, `classType` (default `Sprite`), `scene`, `container`, `key`, `frame`, `ignoreTileset`. + +Animated tiles are defined in Tiled's tileset editor and parsed automatically. Both layer types support them; `TilemapLayerBase` uses `ElapseTimer` to track animation time via `preUpdate`. + +Isometric, hexagonal, and staggered maps -- the map `orientation` property is set from Tiled data and coordinate conversion functions are selected automatically: +```js +const map = this.add.tilemap('iso-map'); +const tileset = map.addTilesetImage('iso-tiles', 'iso-img'); +const layer = map.createLayer('Ground', tileset); +const tile = layer.getIsoTileAtWorldXY(worldX, worldY); // tile at world coords in iso space +// TilemapGPULayer does NOT support iso/hex/staggered -- use TilemapLayer +``` +</common_patterns> + +<gotchas> +- **Tileset name must match Tiled exactly.** The first arg to `addTilesetImage` is the tileset name as defined in Tiled, not the Phaser texture key. Mismatch returns `null` + a console warning. +- **Layer name must match Tiled exactly.** `createLayer` takes the layer name (or index). Group layer children are prefixed `'GroupName/LayerName'`. +- **Each layer can only be created once.** Calling `createLayer` with the same layer ID twice returns `null` + a warning -- the layer data can only be associated with one Game Object. +- **`setCollision` must be called before physics colliders work.** Without marking tiles as collidable, `this.physics.add.collider()` passes through all tiles. +- **TilemapGPULayer is orthographic only** -- no isometric/hexagonal/staggered, and only a single tileset per layer. +- **TilemapGPULayer requires manual texture regeneration.** After `putTileAt` or other edits, call `generateLayerDataTexture()` or the changes won't appear. +- **"Collection of Images" tilesets are not supported.** The Tiled parser requires all tiles in a tileset to be in a single image; embedded tilesets in the exported JSON are required. +- **Tile index -1 means empty.** Many methods return `null` for empty tiles by default. Pass `nonNull: true` to get a Tile object with `index === -1` instead. +- **`insertNull` in the tilemap factory.** `insertNull: true` stores `null` for empty tiles instead of Tile objects with index -1. Saves memory for large sparse maps but prevents dynamic tile placement in empty cells. +- **Tile callbacks only fire with active physics.** `setTileIndexCallback` and `setTileLocationCallback` require a physics collider or overlap between the body and the layer. +- **Layer position and Tiled offset.** If `x` and `y` are not specified in `createLayer`, they default to the layer offset defined in Tiled, not (0, 0). +</gotchas> +</tilemaps> diff --git a/skills/base44-platform-games/references/phaser/time_and_timers.md b/skills/base44-platform-games/references/phaser/time_and_timers.md new file mode 100644 index 0000000..32a4bf6 --- /dev/null +++ b/skills/base44-platform-games/references/phaser/time_and_timers.md @@ -0,0 +1,239 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: time and timers + +<time_and_timers> +Use this when working with timers and time-based events in Phaser 4: the Clock plugin, TimerEvents, delayed calls, looping timers, Timeline event sequencing, pausing time, time scaling, and delta time. + +<quick_start> +`this.time` is the scene's `Clock` instance (registered as the `'Clock'` plugin under the key `time`). It creates and manages `TimerEvent` objects that fire callbacks based on game time. + +```js +// In a Scene's create() method: + +// One-shot delayed call (fires once after 1 second) +this.time.delayedCall(1000, () => { + console.log('One second later'); +}); + +// Repeating timer (fires 5 times, once every 500ms) +this.time.addEvent({ + delay: 500, + callback: () => { console.log('tick'); }, + repeat: 4 // 4 repeats = 5 total fires +}); + +// Infinite loop timer +this.time.addEvent({ + delay: 1000, + callback: this.spawnEnemy, + callbackScope: this, + loop: true +}); +``` +</quick_start> + +<core_concepts> +The Clock (`this.time`) is a Scene-level plugin that tracks game time and updates all of its TimerEvents each frame. Key properties: `now` (current time in ms, equivalent to `time` passed to the scene `update` method), `startTime` (timestamp when the scene started), `timeScale` (multiplier applied to delta time, default `1` — above 1 speeds up all timers, below 1 slows them down, `0` freezes time), and `paused` (when `true`, no TimerEvents are updated). The Clock listens to `PRE_UPDATE` (to flush pending additions/removals) and `UPDATE` (to tick active events). It is automatically shut down and destroyed with the scene. + +A TimerEvent accumulates elapsed time each frame: `elapsed += delta * clock.timeScale * event.timeScale`. When `elapsed >= delay`, the callback fires. After all repeats are exhausted the event is removed from the Clock on the next frame. Config keys: `delay`, `repeat`, `loop`, `callback`, `callbackScope`, `args`, `timeScale`, `startAt`, `paused`. TimerEvent and Clock do not emit EventEmitter events — timers use the `callback` property directly. + +A Timeline (`this.add.timeline`) is a sequencer for scheduling actions at specific points in time. Unlike the Clock (which manages independent timers), a Timeline runs a linear sequence of events keyed by absolute or relative timestamps: + +```js +const timeline = this.add.timeline([ + { at: 0, run: () => { /* immediate */ } }, + { at: 1000, run: () => { /* at 1s */ } }, + { at: 2500, tween: { targets: sprite, alpha: 0, duration: 500 } } +]); +timeline.play(); +``` + +Timelines always start **paused** — you must call `play()` to start them. They are created via the GameObjectFactory and destroyed automatically when the scene shuts down. +</core_concepts> + +<common_patterns> +Delayed call shorthand (fires once, no repeat): +```js +this.time.delayedCall(2000, () => { + this.scene.start('GameOver'); +}); +``` + +Repeating timer with a finite count (`repeat: 9` means 10 total fires — 1 initial + 9 repeats): +```js +const timer = this.time.addEvent({ + delay: 200, + callback: this.fireBullet, + callbackScope: this, + repeat: 9 +}); + +timer.getRepeatCount(); // repeats remaining +timer.getOverallProgress(); // 0..1 across all repeats +``` + +Infinite loop (`repeat: -1` is equivalent to `loop: true`): +```js +const spawner = this.time.addEvent({ + delay: 3000, + callback: this.spawnWave, + callbackScope: this, + loop: true +}); + +spawner.remove(); // stop it later, or spawner.paused = true to pause +``` + +First-fire shortcut with `startAt` (pre-fills elapsed so the first fire happens sooner): +```js +// First fire after 100ms, then every 2s +this.time.addEvent({ + delay: 2000, + callback: this.heartbeat, + callbackScope: this, + loop: true, + startAt: 1900 +}); +``` + +Stopping and removing timers: +```js +const timer = this.time.addEvent({ delay: 1000, loop: true, callback: fn }); + +timer.remove(); // schedules removal next frame, silently expires +timer.remove(true); // fires callback one last time, then expires +this.time.removeEvent(timer); +this.time.removeAllEvents(); +``` + +Pausing and resuming — pause the entire Clock (`this.time.paused = true`) to freeze all timers, or pause a single timer (`timer.paused = true`). + +Time scaling (slow motion / fast forward) — effective scale is `clock.timeScale * event.timeScale`: +```js +this.time.timeScale = 0.5; // slow all timers in this scene to half speed +timer.timeScale = 2; // speed up a single timer to 2x (net 1x here) +``` + +Reading timer state: +```js +timer.getProgress(); // 0..1 for current iteration +timer.getOverallProgress(); // 0..1 across all repeats +timer.getElapsed(); // ms elapsed this iteration +timer.getElapsedSeconds(); // seconds elapsed this iteration +timer.getRemaining(); // ms until next fire +timer.getRemainingSeconds(); // seconds until next fire +timer.getOverallRemaining(); // ms until final fire +timer.getOverallRemainingSeconds(); // seconds until final fire +timer.getRepeatCount(); // repeats left +``` + +A completed `TimerEvent` can be reset with a new config and re-added to the Clock — `reset()` alone does not schedule it: +```js +const timer = this.time.addEvent({ + delay: 1000, callback: this.phase1, callbackScope: this, repeat: 2 +}); + +// Later, after it completes, reconfigure and re-add: +timer.reset({ + delay: 500, callback: this.phase2, callbackScope: this, repeat: 4 +}); +this.time.addEvent(timer); // must re-add; reset() does not schedule it +``` +</common_patterns> + +<timelines> +Timelines excel at sequencing mixed actions — callbacks, tweens, sounds, property sets, and custom events: +```js +const timeline = this.add.timeline([ + { + at: 0, + run: () => { this.title.setAlpha(1); }, + sound: 'intro' + }, + { + at: 2000, + tween: { targets: this.title, y: 100, duration: 1000 }, + sound: { key: 'whoosh', config: { volume: 0.5 } } + }, + { + at: 4000, + set: { alpha: 0 }, + target: this.title, + event: 'INTRO_DONE' + } +]); + +timeline.on('INTRO_DONE', (target) => { /* custom event */ }); +timeline.on('complete', (tl) => { /* all events ran */ }); +timeline.play(); +``` + +Relative timing — `at` is absolute ms from timeline start (default 0); `in` is an offset from the current `elapsed` time (useful when adding events to a running timeline); `from` is an offset from the previous event's start time. Priority: `from` overrides `in`, which overrides `at`. Each `from` is relative to the previous event's resolved time, not the previous `from` value, and events are processed in array order. +```js +const timeline = this.add.timeline([ + { at: 1000, run: stepOne }, // absolute: 1s from start + { from: 500, run: stepTwo }, // relative: 500ms after previous (1.5s) + { from: 1000, run: stepThree } // relative: 1000ms after previous (2.5s) +]); +``` + +Conditional events — if the `if` callback returns `false`, the event is skipped (marked complete but its actions are not executed): +```js +{ at: 5000, if: () => this.player.health > 0, run: () => { this.showBonusRound(); } } +``` + +Looping — the `loop` callback on a TimelineEventConfig fires on repeat iterations (not the first run): +```js +timeline.repeat().play(); // infinite loop +timeline.repeat(3).play(); // loop 3 additional times (4 total runs) +timeline.repeat(false).play(); // no looping +``` + +Control: +```js +timeline.pause(); // freezes elapsed counter and pauses spawned tweens +timeline.resume(); // resumes elapsed counter and unpauses spawned tweens +timeline.stop(); // sets paused=true, complete=true +timeline.reset(); // elapsed=0, all events marked incomplete, starts playing +timeline.isPlaying(); // true if not paused and not complete +timeline.getProgress();// 0..1 based on events completed / total events +timeline.timeScale = 2; // double speed (does NOT affect spawned tweens) +``` + +TimelineEventConfig keys: `at` / `in` / `from` (timing), `run` (callback), `loop` (repeat-iteration callback), `if` (guard, return `false` to skip), `event` (emitted on the Timeline with signature `(target)`), `target` (scope for `run`/`loop`/`if` and target for `set`), `set` (key-value pairs applied to `target`), `tween` (config or instance created when the event fires), `sound` (key or `{ key, config }`), `once` (remove this event after it fires), `stop` (stop the entire timeline when this event fires). The Timeline `'complete'` event fires when all events have run. + +Full cutscene example: +```js +class CutsceneScene extends Phaser.Scene { + create() { + const timeline = this.add.timeline([ + { at: 0, run: () => { console.log('Start!'); } }, + { at: 1000, tween: { targets: this.player, x: 400, duration: 500, ease: 'Power2' } }, + { at: 1500, sound: 'doorOpen' }, + { at: 2000, set: { visible: true }, target: this.door }, + { from: 500, run: () => { console.log('Relative timing'); } }, + { at: 5000, event: 'cutsceneDone', stop: true } + ]); + + timeline.on('cutsceneDone', () => { this.scene.start('GameScene'); }); + timeline.play(); + } +} +``` +</timelines> + +<gotchas> +- **repeat vs total fires.** `repeat: 4` means 5 total callback invocations (1 initial + 4 repeats). A common off-by-one source. +- **Zero delay with repeat throws.** A `TimerEvent` with `delay: 0` and any repeat/loop throws `'TimerEvent infinite loop created via zero delay'`. +- **Timelines start paused.** You must call `timeline.play()` after creation. Forgetting this is a frequent mistake. +- **Timeline timeScale does not affect tweens.** It only scales the timeline's own elapsed counter; tweens created by the timeline run at their own speed. Set tween `timeScale` separately or use the TweenManager. +- **`once` events are removed permanently.** Timeline events with `once: true` are spliced out after firing and will not reappear on `reset()` or when looping. +- **Timer additions are deferred.** `addEvent()` pushes to a pending list processed in `preUpdate`; the timer is not active until the next frame. You can pass an existing `TimerEvent` to `addEvent()`, but it must not be in a completed state. +- **Clock paused vs timeScale 0.** Both freeze timers, but `paused = true` skips the update loop entirely while `timeScale = 0` still runs it with zero delta. Prefer `paused` for a full freeze. +- **callbackScope default.** If you omit `callbackScope`, the TimerEvent itself becomes `this` inside the callback, not the scene. Use arrow functions or pass `callbackScope: this` explicitly. +- **`timer.reset()` does not re-add to Clock.** Calling `timer.reset(config)` reinitializes the timer but does not schedule it — you must call `this.time.addEvent(timer)` again. +- **Scene pause pauses the Timeline.** When a scene is paused (`scene.pause()`), the Timeline's update loop stops too, since Timeline updates are driven by the scene's update step. +</gotchas> +</time_and_timers> diff --git a/skills/base44-platform-games/references/phaser/tweens.md b/skills/base44-platform-games/references/phaser/tweens.md new file mode 100644 index 0000000..5520bcf --- /dev/null +++ b/skills/base44-platform-games/references/phaser/tweens.md @@ -0,0 +1,173 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Phaser 4: tweens + +<tweens> +Use this when animating properties over time in Phaser 4: tweens, tween chains, easing functions, stagger, yoyo, repeat, callbacks, number tweens, and the TweenManager. + +<quick_start> +`this.tweens` is the scene's `TweenManager`, available in every Scene. `add()` creates a tween, adds it to the manager, and starts playback immediately. + +```js +// In a Scene's create() method: +const logo = this.add.image(100, 300, 'logo'); + +// Move the logo to x:600 over 2 seconds +this.tweens.add({ + targets: logo, + x: 600, + duration: 2000, + ease: 'Power2' +}); +``` +</quick_start> + +<core_concepts> +Tween lifecycle: Created -> Active (`onActive`) -> Start Delayed (`delay`) -> Playing (`onStart`, `onUpdate` per frame) -> Yoyo/Repeat (`onYoyo`, `onRepeat`) -> Loop (`onLoop`) -> Complete (`onComplete`, then auto-destroyed unless `persist: true`). + +Tweens are fire-and-forget. They auto-destroy after completion, so you do not need to store a reference unless you want to control them later. Set `persist: true` to keep a tween alive for replay via `tween.play()` / `tween.restart()` — you must then call `tween.destroy()` yourself when done. + +`targets` accepts a single object, an array of objects, or a function returning either. Targets are usually Game Objects but can be any JS object with numeric properties. A tween will not manipulate any property whose name begins with an underscore. + +Property values support several forms: +```js +this.tweens.add({ + targets: sprite, + x: 400, // absolute value + y: '-=100', // relative (subtract 100 from current) + rotation: '+=3.14', // relative (add to current) + alpha: { value: 0, duration: 300, ease: 'Cubic.easeIn' }, // per-property config + scale: [0.5, 1.5, 1], // array: interpolates through values over duration + angle: function (target, key, value, targetIndex, totalTargets, tween) { + return targetIndex * 90; // function: called once per target + }, + duration: 1000 +}); +``` +Array values use linear interpolation by default; override with the `interpolation` config (`'linear'`, `'bezier'`, `'catmull'`). +</core_concepts> + +<common_patterns> +Multiple properties with per-property config: +```js +this.tweens.add({ + targets: this.enemy, + x: { value: 600, duration: 1500, ease: 'Bounce.easeOut' }, + y: { value: 200, duration: 1000, ease: 'Power2' }, + alpha: { value: 0.5, duration: 500, delay: 1000 } +}); +``` + +Yoyo and repeat: +```js +this.tweens.add({ + targets: this.coin, + y: '-=50', + duration: 600, + ease: 'Sine.easeInOut', + yoyo: true, // returns to start value after reaching end + hold: 200, // pause 200ms at the end value before yoyo-ing back + repeat: -1, // -1 = infinite, 0 = play once, 1 = play twice, etc. + repeatDelay: 300 // pause 300ms before each repeat +}); +``` +`repeat` is per-property (a `repeat` of 1 plays the property twice total). `loop` restarts the entire tween from scratch. Use `repeat` for property-level looping, `loop` for tween-level looping. + +Stagger offsets a value across multiple targets via `this.tweens.stagger()`: +```js +delay: this.tweens.stagger(100) // 100ms between each target +delay: this.tweens.stagger(200, { from: 'center' }) // from center outward +delay: this.tweens.stagger([0, 1000]) // distribute 0-1000ms across targets +delay: this.tweens.stagger(500, { grid: [10, 6], from: 'center', ease: 'Cubic.easeOut' }) +``` +StaggerConfig: `start` (offset), `ease`, `from` (`'first'`/`'center'`/`'last'`/index), `grid` ([w, h]). + +A TweenChain plays tweens in sequence — each starts after the previous completes: +```js +this.tweens.chain({ + targets: this.player, + tweens: [ + { x: 300, duration: 1000, ease: 'Power2' }, + { y: 500, duration: 800, ease: 'Bounce.easeOut' }, + { scale: 2, duration: 500 }, + { alpha: 0, duration: 400 } + ], + loop: 1, // loop the entire chain once (plays twice total) + loopDelay: 500, + onComplete: function () { } +}); +``` +Chain-level config supports `loop`, `loopDelay`, `completeDelay`, `paused`, `persist`, and chain-level callbacks. Use `chain.add(tweenConfigs)` to append dynamically. + +Callbacks and events: +```js +this.tweens.add({ + targets: sprite, + x: 600, + duration: 2000, + // All callbacks receive (tween, targets, ...params) + onStart: function (tween, targets) { }, + onUpdate: function (tween, targets) { }, // per-property, per-target, per-frame + onComplete: function (tween, targets) { }, + onCompleteParams: ['extra', 'args'], + callbackScope: this +}); + +// Event emitter style (tweens extend EventEmitter) +tween.on('complete', function (tween, targets) { }); +``` + +Number tweens have no target — they tween between two numeric values: +```js +const counter = this.tweens.addCounter({ + from: 0, + to: 100, + duration: 2000, + ease: 'Linear', + onUpdate: function (tween) { + const value = tween.getValue(); // 0 ... 100 + } +}); +``` + +Controlling and killing tweens: +```js +const tween = this.tweens.add({ targets: sprite, x: 600, duration: 2000, persist: true }); +tween.pause(); tween.resume(); tween.stop(); tween.restart(); +tween.seek(1000); // seek to 1000ms (suppresses events by default) +tween.complete(); // immediately complete; fires onComplete +tween.updateTo('x', 800); // change end value mid-tween + +this.tweens.killAll(); +this.tweens.killTweensOf(sprite); +this.tweens.isTweening(sprite); +this.tweens.pauseAll(); this.tweens.resumeAll(); +this.tweens.setGlobalTimeScale(0.5); +``` +</common_patterns> + +<easing_functions> +Easing names are case-insensitive. Use the string name in the `ease` config property. + +Power aliases: `Power0` = `Linear`, `Power1` = `Quad.easeOut`, `Power2` = `Cubic.easeOut`, `Power3` = `Quart.easeOut`, `Power4` = `Quint.easeOut`. + +Each type supports `.easeIn`, `.easeOut`, `.easeInOut` variants; the bare name defaults to Out. +Types: `Quad`, `Cubic`, `Quart`, `Quint`, `Sine`, `Expo`, `Circ`, `Elastic`, `Back`, `Bounce`. +Special: `Linear` (no easing), `Stepped` (discrete steps — `easeParams: [numSteps]`). +Custom: `ease: function (t) { return t * t; }` where `t` is 0 to 1. +</easing_functions> + +<gotchas> +- Underscore properties are ignored. A tween skips any property whose name starts with `_`. +- Tweens auto-destroy. Using a stored reference after completion may fail. Set `persist: true` to keep it alive, then `destroy()` it yourself when done. +- `repeat` vs `loop`: `repeat` is per-property; `loop` restarts the entire tween. A `repeat` of 1 means the property plays twice total. +- `loop: -1` means `onComplete` never fires. Use `completeAfterLoop()` to end an infinite tween gracefully. +- Seeking suppresses events unless you pass `true` as the third argument to `tween.seek()`. +- Stagger only has a visible effect with multiple targets. +- Destroyed targets: a tween completes early if its target has `isDestroyed === true`. Clean up tweens before destroying objects they reference. +- Manager and tween time scales multiply: effective speed is `managerTimeScale * tweenTimeScale`. Either at 0 freezes the tween. +- Duration cannot be zero — it is clamped to a minimum of 0.01ms. +- TweenChain `targets` are inherited by child tweens unless a child specifies its own. +</gotchas> +</tweens> diff --git a/skills/base44-platform-games/references/threejs/assets_models_textures.md b/skills/base44-platform-games/references/threejs/assets_models_textures.md new file mode 100644 index 0000000..cc698f9 --- /dev/null +++ b/skills/base44-platform-games/references/threejs/assets_models_textures.md @@ -0,0 +1,25 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: assets models textures + +<assets_models_textures> +Use this when loading textures or 3D models, animating loaded models, or generating surfaces procedurally. + +<textures> +- `useTexture` is suspense-aware; the game image tools produce URLs that load straight into it. Keep generated URLs in `src/game/config.js`. +- Tiling a texture across a large ground plane: set `wrapS`/`wrapT` to `THREE.RepeatWrapping` and a `repeat` count. Pixel-art crispness: `magFilter = THREE.NearestFilter`. Color textures that look washed out: set `colorSpace = THREE.SRGBColorSpace` (the old `.encoding`/`sRGBEncoding` API is gone — never emit it). +- Procedural surfaces (stripes, grids, text-on-surface): draw a small offscreen 2D canvas once (memoized, never per frame) and wrap it in `THREE.CanvasTexture`. +</textures> + +<models> +- `useGLTF` loads a model (suspends); mount its `scene` with `<primitive object={...} />`. A loaded GLTF scene can be mounted only ONCE — render additional copies with drei `<Clone object={...} />`. +- Animations: `useAnimations(animations, groupRef)` exposes `actions`; crossfade states on gameplay events by fading out the current action and `reset().fadeIn().play()` on the next. Action names come from the file — log the keys of `actions` when unsure. +- Loaded models don't cast shadows by default: traverse the scene once and set `castShadow` on each mesh. +- Loaders cache by URL — using the same URL in ten components fetches once. Start downloads early with `useGLTF.preload`/`useTexture.preload` at module scope; drei `<Preload all />` inside the Canvas uploads everything to the GPU up front, killing first-encounter hitches. +</models> + +<sourcing> +Only reference model/texture URLs that actually exist (uploaded by the user or generated by the image tools) — there is no built-in asset library, and hallucinated URLs render nothing. When no model exists, build characters/props from primitives composed in a `<group>` (see the visuals guidance) — it looks intentional and ships now. +</sourcing> +</assets_models_textures> diff --git a/skills/base44-platform-games/references/threejs/cameras.md b/skills/base44-platform-games/references/threejs/cameras.md new file mode 100644 index 0000000..0dc7bdb --- /dev/null +++ b/skills/base44-platform-games/references/threejs/cameras.md @@ -0,0 +1,27 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: cameras + +<cameras> +Use this when setting up game cameras: follow/chase cameras, first-person, fixed/isometric angles, orbiting, and camera feedback (shake). + +<follow_camera> +- When the user didn't specify a camera style, default to a damped follow rig: a component whose `useFrame` eases `state.camera.position` toward an offset from the target each frame and then `lookAt`s the target. +- Use frame-rate-independent exponential smoothing — lerp with a factor of `1 - Math.exp(-k * delta)` (k ≈ 4; higher = snappier). A constant-factor lerp is a trap: it speeds up on high-refresh screens. +- The target position comes from the player RigidBody's `translation()` (or the mesh position in non-physics games); reuse one module-level scratch Vector3 rather than allocating per frame. +- Vary the offset for genre — top-down (0, 14, ~0) (the ~0 keeps the lookAt off dead-vertical, which would leave screen-up — and with it the key mapping — ambiguous), side-scroller (0, 3, 12) with a fixed lookAt height, isometric-style (8, 10, 8) with a lower fov (~35). Keep the offset in `src/game/config.js`. +- The offset either stays FIXED in world space (top-down, isometric, side-view, lane runner — the camera follows position only, never rotated) or rotates with the target's heading (chase rig — rotate the offset by the target's quaternion). In a chase rig, "behind" means a positive local-Z offset for a −Z-facing target, looking at a point ahead of it along −Z; left/right mirrored in play almost always means the camera is in FRONT of the facing — fix the offset sign. In a fixed rig, mirrored or 90°-rotated controls are a key→world mapping bug — fix the input mapping, not the camera. +</follow_camera> + +<control_helpers> +- `<OrbitControls makeDefault />` — inspect/orbit games (the template default). Constrain for gameplay: `minDistance`/`maxDistance`, `maxPolarAngle` just under PI/2 so the camera never goes below ground, `enablePan={false}`. In aim-and-power games the shot direction derives from the orbit camera's azimuth or the pointer hit point — one recorded source, like any other direction convention. +- `<PointerLockControls />` — first-person mouse look; the canvas captures the pointer on click, Esc releases. Pair with a capsule player body; show a "click to play" overlay hint while unlocked (`onLock`/`onUnlock`). +- A follow rig and OrbitControls fight each other — pick one. When mixing modes (orbit in menus, follow in play), gate them on the game phase. +- Cutscene-style pans and zoom-to-target: drei `<CameraControls makeDefault />` — its async API (`setLookAt` with the transition flag) animates smoothly. +</control_helpers> + +<feedback> +drei `<CameraShake>` adds impact feedback — mount it briefly on hits/explosions (small `maxYaw/maxPitch/maxRoll`, `decay` on), triggered by a short-lived state from an EventBus event. Small and brief reads as impact; constant shake reads as broken. +</feedback> +</cameras> diff --git a/skills/base44-platform-games/references/threejs/character_controller.md b/skills/base44-platform-games/references/threejs/character_controller.md new file mode 100644 index 0000000..bb888af --- /dev/null +++ b/skills/base44-platform-games/references/threejs/character_controller.md @@ -0,0 +1,25 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: character controller + +<character_controller> +Use this when the player directly controls a walking/jumping character (platformer, third-person, first-person). This is the most failure-prone 3D pattern — follow these rules exactly. + +<capsule_recipe> +A dynamic RigidBody with `colliders={false}` plus an explicit `<CapsuleCollider>` (args are [halfHeight, radius]; capsules slide over small steps where boxes catch), driven by velocity in `useFrame`: +- `lockRotations` — physics must never tip the capsule over. Face the move direction by rotating the child MESH toward the velocity heading, not the body. +- Move with `setLinvel`, setting X/Z from input but PRESERVING the current Y velocity (read `linvel()` first) — overwriting Y kills gravity and jumping. +- Jump only when grounded: cast a short downward ray (`world.castRay` from `useRapier`) from just below the capsule base (~halfHeight + radius below the body's `translation()`); if it hits within ~0.2, set an upward Y velocity. +- `canSleep={false}` — a sleeping body ignores input until something wakes it. +- The wake flag (second argument `true`) on every `setLinvel`/`applyImpulse` call. +- Fall recovery: when `translation().y` drops below the world floor (e.g. -20), teleport back with `setTranslation` and zero the velocity. +- Camera-relative movement (rotating third-person camera): rotate the input vector by the camera yaw (from `camera.getWorldDirection`) before applying it. +</capsule_recipe> + +<ecctrl_fast_path> +For a full-featured third-person character (slopes, steps, moving platforms, camera follow modes) skip hand-rolling: install `ecctrl` with `install_npm_package` — the official pmndrs floating-capsule controller built on this exact stack. Wrap the character model in `<Ecctrl>` inside `<Physics>`; it expects the drei `KeyboardControls` action names `forward/backward/leftward/rightward/jump/run`, and its `<EcctrlJoystick />` (rendered OUTSIDE the Canvas, before it) adds mobile touch controls automatically. + +Prefer ecctrl when the request is a real third-person game; prefer the capsule recipe for simple arcade movement or side-view platformers where its camera rig is unwanted. +</ecctrl_fast_path> +</character_controller> diff --git a/skills/base44-platform-games/references/threejs/enemies_and_movement.md b/skills/base44-platform-games/references/threejs/enemies_and_movement.md new file mode 100644 index 0000000..595f93f --- /dev/null +++ b/skills/base44-platform-games/references/threejs/enemies_and_movement.md @@ -0,0 +1,31 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: enemies and movement + +<enemies_and_movement> +Use this when scripting non-player motion: chasing/patrolling enemies, moving platforms, doors/elevators, orbiting hazards, joints, and worlds that flow past the player. + +<world_flow> +When the world moves past a mostly-stationary player (rather than the player traversing a static world): +- Keep the player near the origin and move the world: spawn ahead at negative Z (the horizon), flow toward positive Z past the camera, cull a few units behind it. On screen the world flows from the horizon toward the bottom edge — ground sliding toward the horizon reads as moving backwards. +- Track bookkeeping stays naturally positive (distance run, segment index) — keep it positive, and convert to world space in exactly ONE shared formula that puts ahead at negative Z (`worldZ = traveled − trackZ`), used by EVERY flowing system (ground, obstacles, pickups, scenery). Dropping the negation because positive indices feel cleaner — or re-deriving the formula per file — is how one system ends up negated while the rest are correct and the scene tears in two directions. +- Ahead = negative relative Z, so visibility/cull windows run from −spawnDistance to a few units positive — a mirrored window makes objects invisible until they are already on top of the player. +</world_flow> + +<kinematic_movement> +- Script-driven movers are `type="kinematicPosition"` bodies advanced each frame with `setNextKinematicTranslation` — they push/carry dynamic bodies (including the player) but nothing pushes them back. Scale all motion by `delta`. +- Chase: each frame, take the direction from the enemy's `translation()` to the player's on the ground plane (zero the Y component), normalize, scale by speed × delta, and set the next translation. Stop advancing inside melee range and attack instead. Reuse one module-level scratch Vector3 — no per-frame allocation. +- Turret targeting, AoE, nearest-enemy scans: iterate the shared live registry (`world.enemies` — see game_state_and_flow state_home) inside `useFrame`; enemies register on spawn, deregister on death. Never scan a useState array. +- Patrol, platforms, elevators, spinners: drive position from `state.clock.elapsedTime` (sine/cosine of it) rather than accumulating your own offsets — deterministic, no drift. Rotating movers use `setNextKinematicRotation` with a quaternion. +- Simple AI state machines (patrol → chase when within aggro radius → attack in melee range) are a phase field in a ref checked at the top of `useFrame` — no library needed. +</kinematic_movement> + +<dynamic_enemies> +Enemies that should be shoved by explosions/hits are dynamic capsules like the player (lockRotations, velocity steering via `setLinvel` toward the target instead of kinematic translation). Kinematic = reliable pursuit that cannot be body-blocked; dynamic = physical comedy and knockback. Pick per game feel. +</dynamic_enemies> + +<joints> +Joints connect two RigidBody refs: `useRevoluteJoint` (hinge — doors, flippers), `useSphericalJoint` (ball-socket — swinging ropes, a wrecking ball on a fixed anchor), `useFixedJoint`, `usePrismaticJoint` (sliders). Chain segments by joining successive bodies. Reach for joints only when the physics should visibly articulate — most gameplay motion is the kinematic scripting above. +</joints> +</enemies_and_movement> diff --git a/skills/base44-platform-games/references/threejs/game_state_and_flow.md b/skills/base44-platform-games/references/threejs/game_state_and_flow.md new file mode 100644 index 0000000..72fc65a --- /dev/null +++ b/skills/base44-platform-games/references/threejs/game_state_and_flow.md @@ -0,0 +1,36 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: game state and flow + +<game_state_and_flow> +Use this when structuring game phases (menu → playing → game over), score/health state, pause, restart, and saving progress. + +<state_home> +Two kinds of state, two homes: +- Discrete/UI state (phase, score, lives, unlocks) — React state in the overlay, updated via EventBus events from gameplay. Low frequency, drives DOM. +- Per-frame gameplay state — refs and module objects read inside `useFrame`. This covers scalars (positions, velocities, timers, mid-run combo counters) AND entity collections with their mutable fields: the live arrays of enemies/towers/projectiles and each entity's hp/target/cooldown. Never React state — beyond re-render stutter, `useFrame` callbacks capture the array from the render that created them, so a tower reading a useState `enemies` list targets a STALE snapshot: damage lands on ghosts and kills never register. Writing game logic as `setState(prev => ...)` updaters (or nesting one setState inside another) is the same failure. +- When state must be BOTH (read every frame in gameplay AND shown reactively in UI), install `zustand` with `install_npm_package`: components subscribe with the hook selector, `useFrame` reads via `getState()` without re-rendering. For simple games, EventBus + a module object is enough — don't add zustand for a single score counter. +- Systems that iterate many entities each frame share ONE live registry, mutated in place: +```js +// src/game/3d/world.js — module scope (or one useRef/context at the scene root) +export const world = { enemies: [], towers: [], projectiles: [] }; +``` +Spawn = push, death = splice or flag-and-reuse (see projectiles_and_pooling); each system's `useFrame` (targeting, projectiles, wave spawner) reads `world.enemies` live — no props, no copies. Entries hold plain fields plus refs to their body/mesh. The HUD never reads the registry: gameplay emits discrete or throttled (100-250ms) EventBus/zustand snapshots and React renders that copy. On restart, empty the arrays explicitly — the `key={runId}` remount does not clear module scope. +</state_home> + +<phases> +- Gate on the phase: the `paused` prop on `<Physics>` when not playing, conditional-render the level subtree, and early-return guards at the top of gameplay `useFrame` callbacks. +- The Canvas stays mounted through every phase — menus render as overlay on top of an idle scene, which also looks better than an empty canvas. +- Restart: remount the play-world subtree with a key (`key={runId}`, incremented on restart). Remounting resets every body, ref, and timer to initial JSX state in one move; manual per-object reset code is a bug farm. This is the ONE place mass unmount/remount is correct — once per run, not per frame. +- UI countdowns and run timers default to the overlay (interval or elapsed-from-start-timestamp); a timer that gameplay itself consumes (physics-coupled time limits, slow-motion) belongs in the loop/physics step instead. +</phases> + +<persistence> +Only persistence path: define a Base44 entity schema (for example `base44/entities/HighScore.jsonc`) and read/write it with the SDK (`base44.entities.*`) from React overlay/page code. +- Do not persist game data in `localStorage`, auth profile fields, refs, module globals, config files, or Three.js/R3F state. +- Gameplay emits final/progress data through `EventBus` at phase boundaries; React calls `base44.entities.<Entity>.create/filter/update/delete`. +- React passes loaded entity data back into gameplay as runtime state. Never call the SDK inside `useFrame`, physics callbacks, or render-loop systems. +- Leaderboards use open `read` and owner-scoped writes; never pass owner fields manually. +</persistence> +</game_state_and_flow> diff --git a/skills/base44-platform-games/references/threejs/input_and_controls.md b/skills/base44-platform-games/references/threejs/input_and_controls.md new file mode 100644 index 0000000..38e3947 --- /dev/null +++ b/skills/base44-platform-games/references/threejs/input_and_controls.md @@ -0,0 +1,28 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: input and controls + +<input_and_controls> +Use this when handling player input: keyboard movement/actions, clicking or hovering 3D objects, and touch controls on mobile. + +<keyboard> +- Map physical keys to named actions once with drei `<KeyboardControls map={...}>` wrapping the scene root inside the Canvas — actions like forward/back/left/right/jump, each listing its key codes (`KeyW`, `ArrowUp`, `Space`, ...). +- For per-frame input, use the imperative tuple form exactly: `const [, getKeys] = useKeyboardControls();`, then inside `useFrame` read fresh state with `const keys = getKeys();`. This does NOT re-render. Convert the booleans to a -1/0/1 axis and scale movement by `delta` (or feed the physics API). +- The selector form returns the selected value directly: `const jump = useKeyboardControls((s) => s.jump);` makes `jump` a boolean and re-renders on change, so use it only for discrete UI-ish reactions, never for movement. NEVER call a selector result — `const getKeys = useKeyboardControls((s) => s); getKeys()` is invalid because `getKeys` is a state object, not a function. +- Apply axis signs in ONE place, derived from the convention recorded in config. The trap is double negation: an axis computed `left − right` and then negated again downstream "for feel" leaves A steering right. +- Two control models, picked per game and independent of the camera: DIRECT ( twin-stick, platformer — a key means a direction) and STEERED (car, plane, boat — A/D turn the heading, W/S throttle along it, under ANY camera including top-down). After wiring, trace one key end-to-end: direct — D must move toward screen-right as the player sees it (a fixed world direction under a fixed camera; rotated by the camera yaw under a rotating chase/first-person camera); steered — D must turn the vehicle toward ITS OWN right (clockwise seen from above; increasing `rotation.y` turns LEFT), even when it faces the camera and that reads as screen-left. +- Match the scheme to the vehicle when the user didn't specify: aircraft and ships don't reverse — W/S are throttle up/down (floored at zero or a stall speed), not forward/backward; cars reverse; runners steer lanes and never move backwards. Under direct control, rotate the avatar MESH to face its velocity (or the aim, in twin-stick) — the keys move the body, not the facing. +</keyboard> + +<pointer> +- Meshes take pointer events directly (`onClick`, `onPointerOver/Out/Down`) — R3F raycasts for you; no manual Raycaster. `e.stopPropagation()` blocks hits on objects behind; `e.point` is the world-space hit position — the standard way to aim or place things by click (click-to-move on a ground plane is just an `onClick` on it). +- Set `document.body.style.cursor` in `onPointerOver`/`Out` for hover affordance. +</pointer> + +<touch> +- If the user didn't specify mobile controls, a DOM joystick in the React overlay is a good default — but match the genre: tap-to-move/tap targets suit point-and-click and board games, swipe suits runners. +- Wire touch input through a shared module object (e.g. `src/game/input.js` exporting a mutable state the overlay writes on touch events and gameplay reads in `useFrame`) — never React state per move event. +- Keep touch targets comfortably large (~64px). Show touch controls only on coarse pointers: `window.matchMedia('(pointer: coarse)').matches`. +</touch> +</input_and_controls> diff --git a/skills/base44-platform-games/references/threejs/performance.md b/skills/base44-platform-games/references/threejs/performance.md new file mode 100644 index 0000000..1698330 --- /dev/null +++ b/skills/base44-platform-games/references/threejs/performance.md @@ -0,0 +1,33 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: performance + +<performance> +Use this when a scene has many objects, when frame rate matters (it always does in games), or when reviewing gameplay code for jank. + +<the_rules> +1. No React state changes per frame — mutate refs in `useFrame`; React state only for discrete events. This is the #1 3D-game jank source, and for entity collections (enemy/projectile arrays, per-entity hp) it is also a correctness bug: closures read stale snapshots (see game_state_and_flow). +2. No allocations in hot loops — a `new` Vector3 per frame churns the GC into stutters. Hoist scratch objects to module scope (or `useMemo`) and reuse them via `set(...)`. +3. Share geometries and materials. Inline geometry/material JSX per mesh creates one of each per mount — fine for a dozen meshes, deadly for hundreds. Create them once (module scope or memoized) and pass via the `geometry`/`material` props. +4. Scale movement by `delta` — code tuned at 60fps without delta runs double speed on 120Hz screens. +</the_rules> + +<many_objects> +- Each mesh is a draw call; hundreds of identical objects (trees, coins, tiles, debris) belong in ONE instanced draw call — drei `<Instances>` (one geometry + material) with an `<Instance>` per item, each still taking position/rotation/scale/color and pointer events. Set `limit` to the maximum count. +- The physics twin is `InstancedRigidBodies` (an `instances` array prop wrapping one `instancedMesh`) for piles of physical debris. +- Distant detail: drei `<Detailed>` with per-distance children swaps LOD levels automatically. +</many_objects> + +<measuring_and_degrading> +- drei `<StatsGl />` while developing when performance is in question; judge draw calls and fps, then remove it. +- Auto-degrade on weak devices with `<AdaptiveDpr>`/`<PerformanceMonitor>` (see scene_and_rendering). +- Raycast-heavy scenes (constant pointer hover over complex meshes) get free speed from wrapping the scene in drei `<Bvh>`. +- Shadow maps are the silent budget-eater: a single 2048 map is usually enough; prefer turning `castShadow` off on small clutter over growing the map. +</measuring_and_degrading> + +<react_shape> +Keep the component tree stable during play: conditional-render UI states, but never map gameplay entities off arrays that change every frame, and never key things by per-frame values — both force remounts (see the pooling rule in projectiles_and_pooling). If a component re-renders every frame, something reads per-frame state reactively; move that read into `useFrame`/`getState()`. +Debugging tells that gameplay data lives in useState: damage that never applies, turrets targeting already-dead enemies, `setState(prev => ...)` updaters as game logic (or nested inside other updaters). The fix is always the same — move the collection to a ref/module registry and keep React as a throttled HUD mirror. +</react_shape> +</performance> diff --git a/skills/base44-platform-games/references/threejs/physics_dynamics.md b/skills/base44-platform-games/references/threejs/physics_dynamics.md new file mode 100644 index 0000000..d157d42 --- /dev/null +++ b/skills/base44-platform-games/references/threejs/physics_dynamics.md @@ -0,0 +1,26 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: physics dynamics + +<physics_dynamics> +Use this when configuring rigid bodies, colliders, and forces with @react-three/rapier — stacking, bouncing, knockback, vehicles, physical props. + +<bodies_and_colliders> +- Body types: `type="fixed"` for level geometry, dynamic (the default) for anything gravity and forces should move, `type="kinematicPosition"` for code-driven movers (it pushes dynamic bodies but is immovable itself). +- Collider auto-generation from child meshes via the `colliders` prop: `"cuboid"` (default) | `"ball"` | `"hull"` (convex wrap — the right choice for most dynamic props) | `"trimesh"`. HARD RULE: `trimesh` only on `fixed` bodies (concave level geometry) — a dynamic trimesh is slow and tunnels. +- Compound or offset shapes: `colliders={false}` plus explicit `<CuboidCollider>`/`<BallCollider>`/`<CapsuleCollider>`/`<CylinderCollider>` children. Explicit collider `args` are HALF-extents. +- Material feel per body/collider: `restitution` (bounciness), `friction`, `mass` (or `density`). `ccd` on small fast bodies stops them tunneling through walls. The `debug` prop on `<Physics>` renders all colliders — dev only. +</bodies_and_colliders> + +<moving_bodies> +- Never mutate mesh positions inside a RigidBody and never move a dynamic body via props after mount — always the body ref API: `applyImpulse` (instant kick — jumps, hits, explosions), `setLinvel` (direct control), `applyTorqueImpulse`/`setAngvel` (spin), `setTranslation` (teleport/respawn — also zero the velocity). +- Every one of those calls takes a wake flag as its last argument — pass `true`; sleeping bodies ignore forces. +- Impulses scale with mass: the same impulse that launches a mass-1 crate nudges a mass-10 one. Read state with `translation()`, `linvel()`, `rotation()`. +- Common tuning: floaty jumps → stronger gravity (around -20) or extra downward force while airborne; jittery stacks → bigger colliders and more mass; everything slides → raise `friction` on both surfaces. +</moving_bodies> + +<per_step_logic> +Physics steps at a fixed 1/60 timestep decoupled from rendering. Logic that must see every step (custom gravity, force fields, speed clamps) belongs in `useBeforePhysicsStep`, not `useFrame`. Pause the whole simulation with the `paused` prop on `<Physics>`. +</per_step_logic> +</physics_dynamics> diff --git a/skills/base44-platform-games/references/threejs/projectiles_and_pooling.md b/skills/base44-platform-games/references/threejs/projectiles_and_pooling.md new file mode 100644 index 0000000..7c127d9 --- /dev/null +++ b/skills/base44-platform-games/references/threejs/projectiles_and_pooling.md @@ -0,0 +1,25 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: projectiles and pooling + +<projectiles_and_pooling> +Use this when things spawn and despawn rapidly: bullets, thrown objects, falling debris, wave spawns. The core rule: NEVER mount/unmount React components per spawn — pool them. + +<pool_pattern> +- Mount a fixed pool of RigidBodies once (an array render with stable keys, refs collected into a ref array). "Spawning" teleports a parked body in (`setTranslation`) and gives it velocity (`setLinvel`); "despawning" parks it again. +- Park inactive bodies far below the level (staggered positions so they don't overlap), with `gravityScale={0}` and zeroed velocity so they stay put; restore gravityScale on activation if the object should fall. +- Recycle round-robin: keep a next-slot index; the oldest active body is simply reused when the pool wraps. +- Despawn on contact via `onCollisionEnter` (apply the hit, then park the payload's target body) AND by lifetime — a `useFrame` sweep parking anything active older than a few seconds (spawn times in a ref array). +- `ccd` on bullets stops fast ones tunneling through thin walls. +- The same pattern serves enemies, debris, and floating damage numbers. +</pool_pattern> + +<hitscan> +- Instant-hit weapons and line-of-sight checks skip projectile bodies entirely — cast a ray through the physics world: `world.castRay` from `useRapier`, constructing the ray with the rapier `Ray` class from an origin and direction. +- The hit result's field is `timeOfImpact` (the old name `toi` was renamed — do not emit it); get the world-space point via the ray's `pointAt(timeOfImpact)`, and identify what was hit through the hit collider's parent body (name/userData). +- Exclude the shooter from its own ray via the cast's filter argument (pass the shooter's rigid body as the excluded body). +- Aim from the camera with `camera.getWorldDirection`. +- Decision rule: hitscan + a pooled tracer/impact effect for anything faster than the eye; pooled bodies when travel time and arcs are part of the gameplay. +</hitscan> +</projectiles_and_pooling> diff --git a/skills/base44-platform-games/references/threejs/scene_and_rendering.md b/skills/base44-platform-games/references/threejs/scene_and_rendering.md new file mode 100644 index 0000000..717e225 --- /dev/null +++ b/skills/base44-platform-games/references/threejs/scene_and_rendering.md @@ -0,0 +1,29 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: scene and rendering + +<scene_and_rendering> +Use this when configuring the Canvas, lights, shadows, atmosphere (sky, fog, background), or render quality for a 3D game. + +<canvas_and_lights> +- One persistent `<Canvas>` in `src/game/3d/Game.jsx` (never remount it), configured via props: `shadows`, `dpr={[1, 2]}`, and the default `camera` (fov/near/far/position). One `<Suspense fallback={null}>` at the scene root covers all async content (`<Physics>`, textures, models). +- Light the scene so it reads clearly by default — unless the user asks for a dark, moody, or horror look. Give it a motivated key light chosen for the space: a sun-like `directionalLight` for open/outdoor scenes, a `spotLight` or `pointLight` for indoor or localized pools — positioned and powered to actually illuminate the play area and cast readable shadows. +- Add an `ambientLight` or `hemisphereLight` (sky/ground colors) as gentle fill ONLY — enough to keep shadowed faces off pure black, never to light the scene (fill is flat and shadowless). A hemisphere fill reads better than a second directional. +- A scene that renders dark almost always means the key is too weak or aimed away from the action, or the palette itself is near-black (see render_quality) — fix the key or the albedo rather than piling on ambient or cranking exposure. +- Shadows silently missing means one of three flags is absent — Canvas `shadows`, `castShadow` on the caster, `receiveShadow` on the receiver — or the play area outgrew the directional light's shadow camera box (`shadow-camera-left/right/top/bottom` must cover it). Bump `shadow-mapSize` for crisper edges; a small negative `shadow-bias` fixes striped self-shadowing ("shadow acne"). +</canvas_and_lights> + +<atmosphere> +- Set the scene backdrop with `<color attach="background" />` and add `<fog attach="fog" />` matched to the background color — fog is the cheapest depth cue and hides the far clip. +- Fog is a depth cue, not a curtain: start the fog band beyond the playfield at the camera's typical distance — a near band matched to a dark background fades the whole play area toward black. +- drei quick picks: `<Sky>` (procedural daytime sky), `<Stars>` (night/space). `<Environment preset>` adds image-based lighting BUT downloads its HDRI from a CDN at runtime — prefer `<Sky>` + lights; use Environment only when realistic reflections matter and the network dependency is acceptable. +</atmosphere> + +<render_quality> +- drei `<AdaptiveDpr pixelated />` drops resolution automatically while the frame rate is low; `<PerformanceMonitor onDecline/onIncline>` can drive coarser quality toggles (shadow size, effect density). +- Color management is automatic in current three.js; R3F defaults to ACESFilmic tone mapping at exposure 1. `toneMappingExposure` (set via the Canvas `gl` prop) is the master brightness dial — a small nudge shifts overall mood, but it is not a substitute for lighting the scene; fix darkness at the light, not the exposure. +- Lighting cannot rescue near-black materials: give large surfaces (ground, boards, walls) mid-tone or brighter albedo by default — unless the user asks for a dark or night look — and save near-black for accents. +- Metallic/reflective materials need an environment to reflect or they render black — keep default `meshStandardMaterial` low-`metalness` unless an `<Environment>` or bright surroundings are present. +</render_quality> +</scene_and_rendering> diff --git a/skills/base44-platform-games/references/threejs/text_hud_ui.md b/skills/base44-platform-games/references/threejs/text_hud_ui.md new file mode 100644 index 0000000..82a94bc --- /dev/null +++ b/skills/base44-platform-games/references/threejs/text_hud_ui.md @@ -0,0 +1,29 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: text hud ui + +<text_hud_ui> +Use this when showing text or UI: world-anchored labels, floating damage numbers, health bars above heads — and the boundary with the React overlay. + +<the_boundary> +This section covers text/UI attached to points in the world (name tags, damage numbers, signs, health bars over enemies); screen-space UI belongs in the React overlay per the architecture instructions — never as meshes, never via drei. When in doubt: no world position → overlay. +</the_boundary> + +<world_text> +- drei `<Text>` renders crisp SDF text at any size and angle (not blurry canvas sprites) — `fontSize`, `color`, `anchorX/anchorY`, and `outlineWidth`/`outlineColor` for readability against the scene. +- Wrap in `<Billboard>` to always face the camera (name tags, indicators). +- 3D extruded `<Text3D>` needs a bundled font JSON — prefer flat `<Text>` unless the text IS the centerpiece. +</world_text> + +<damage_numbers> +Floating combat text is a pooled set of `<Text>` items animated in `useFrame` (rise + fade via the text material's opacity, with `transparent` on), activated from an EventBus event carrying a position — same pooling rule as projectiles: never mount/unmount per hit; park when invisible. +</damage_numbers> + +<world_html> +- drei `<Html>` places real DOM at a 3D point — the pragmatic choice for styled micro-UI like health bars (a Tailwind-styled div). `center` anchors the middle, `distanceFactor` scales it with distance so it behaves like a world object, `occlude` hides it behind geometry. +- Update it imperatively (a ref setting styles) from events — not React state per frame. +- Each Html instance is a live DOM element — fine for a handful (bosses, nearby enemies), wrong for hundreds; beyond that use a thin mesh scaled by value instead. +- R3F hooks don't work inside the Html subtree (it renders to the DOM, outside the Canvas reconciler). +</world_html> +</text_hud_ui> diff --git a/skills/base44-platform-games/references/threejs/triggers_and_collisions.md b/skills/base44-platform-games/references/threejs/triggers_and_collisions.md new file mode 100644 index 0000000..76a6e14 --- /dev/null +++ b/skills/base44-platform-games/references/threejs/triggers_and_collisions.md @@ -0,0 +1,28 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: triggers and collisions + +<triggers_and_collisions> +Use this when gameplay reacts to contact: pickups, damage zones, goals, checkpoints, projectile hits, "what did I touch?". + +<identifying_bodies> +Give bodies a `name` (exposed on the event payload's three object) or `userData` for richer typing — this is how handlers know WHAT was hit: in the event payload, check `other.rigidBodyObject?.name` or `other.rigidBody?.userData`. +</identifying_bodies> + +<sensors> +- A collider with the `sensor` prop detects overlap but has no physical presence — THE gameplay trigger primitive (coins, kill zones, goal lines, checkpoints, aggro ranges). Sensors fire `onIntersectionEnter` / `onIntersectionExit` (solid colliders do not). +- The standard pickup: a `fixed` RigidBody with `colliders={false}`, an explicit sensor collider, and an `onIntersectionEnter` that checks the other body's name is the player, then emits an EventBus event (score/HUD updates on the React side) and hides the item via React state — a discrete event, so state is fine here. +- A large sensor box under the level is the standard fall-death detector. +</sensors> + +<solid_collisions> +- Solid bodies fire `onCollisionEnter` / `onCollisionExit` (on the RigidBody or a specific collider). Check who hit via the payload's name/userData as above. +- Debounce repeated contacts — an enemy grinding against the player fires enter/exit rapidly; track the last-hit time in a ref and ignore hits inside the invulnerability window. +- `onContactForce` reports impact magnitude — for "break on hard landing" style rules. +</solid_collisions> + +<filtering> +`collisionGroups` with the `interactionGroups(group, [collidesWith...])` helper (imported from @react-three/rapier) when categories must pass through each other — e.g. enemy projectiles ignoring enemies. Groups 0-15. Prefer plain name checks in handlers until pass-through behavior is actually required. +</filtering> +</triggers_and_collisions> diff --git a/skills/base44-platform-games/references/threejs/vfx_and_juice.md b/skills/base44-platform-games/references/threejs/vfx_and_juice.md new file mode 100644 index 0000000..266f196 --- /dev/null +++ b/skills/base44-platform-games/references/threejs/vfx_and_juice.md @@ -0,0 +1,36 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 games: Three.js: vfx and juice + +<vfx_and_juice> +Use this when adding feedback and polish ("juice"): particles, trails, glows, hit flashes, audio, win-moments. When the user hasn't specified polish, a light juice pass is usually what makes a simple game feel finished — suggest it rather than skipping it silently. + +<drei_effects> +- `<Sparkles>` — ambient magic/pickup shimmer. +- `<Trail>` — motion readability on anything fast; it follows the mesh rendered as its child. +- `<Outlines>` — selection/highlight, rendered inside a mesh. +- One-shot bursts (explosion, pickup pop): a pooled `<Sparkles>` repositioned to the event point for ~0.5s — pooled and reused like projectiles, never created per event. +</drei_effects> + +<hit_feedback> +A menu to pick from — each is a few lines; when the user didn't specify hit feedback, one or two of these on significant hits is a good default: +- Camera shake: brief `<CameraShake>` (see the cameras section). +- Flash: swap the target's material color/emissive for ~100ms (ref + timeout restoring the original). +- Scale pop: damp the mesh scale up to ~1.3 and back in `useFrame`. +- Hit-stop: freeze physics a beat — the `paused` prop on `<Physics>` for 60-100ms on big hits. +- Floating damage number (see text_hud_ui). +</hit_feedback> + +<audio> +drei `<PositionalAudio>` attaches 3D sound to an object (`loop` for ambience; call `play()` on its ref for one-shots). Browsers block audio until a user gesture — start playback only after the first click/keydown (the start-screen "Play" button is the natural unlock). Screen-space UI sounds can be a plain Audio element on the overlay side. Only reference audio URLs that actually exist (user-uploaded or provided) — there is no built-in sound library; when none exist, skip audio rather than pointing at dead URLs. +</audio> + +<win_moments> +`canvas-confetti` is already a dependency — fire it from the React overlay on win/level-complete EventBus events. If the user didn't specify a win moment, confetti + a camera move toward the goal (drei CameraControls transition) + a scale-pop on the goal object is a combo that works well. +</win_moments> + +<postprocessing> +For bloom (glowing projectiles/neon), vignette, or chromatic aberration, install `@react-three/postprocessing` with `install_npm_package`: one `<EffectComposer>` as the last child inside the Canvas, with effect components inside (all effects in the composer merge into a single pass). For selective glow, set `luminanceThreshold` to 1 on `<Bloom>` (with `mipmapBlur`) so only overbright objects bloom, and make materials overbright via `emissive` + `emissiveIntensity` above 1 (or `toneMapped={false}` on plain colors). Skip postprocessing on visibly low-end targets (pair with PerformanceMonitor). +</postprocessing> +</vfx_and_juice> diff --git a/skills/base44-platform-integrations-sdk/SKILL.md b/skills/base44-platform-integrations-sdk/SKILL.md new file mode 100644 index 0000000..2703c8f --- /dev/null +++ b/skills/base44-platform-integrations-sdk/SKILL.md @@ -0,0 +1,24 @@ +--- +name: base44-platform-integrations-sdk +description: "Base44 built-in integrations on the SDK client: InvokeLLM, SendEmail, file uploads (public and private), signed URLs, data extraction and image generation, with every Core endpoint's input schema, plus analytics event tracking. Use when a feature needs AI, email, files or analytics without a third-party service." +--- +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 integrations sdk + +Core integrations are callable from the browser SDK and from server code; the endpoint schemas below are the ones the platform serves today. + +## Pick the reference for this app + +Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (over the Base44 MCP `execute_api` +tool, or with your Base44 CLI token); it returns the framework, app type, runtime, +entitlements and the exact reference files to read. Without it, detect from the project files: +`src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a +native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. + +| Profile | Read | +|---|---| +| Web app, Vite SPA (the default web template) | [Integrations and analytics SDK (web apps)](references/web.md) | +| Web app, TanStack Start | [Integrations and analytics SDK (web apps)](references/web.md) | +| Native mobile app (Expo, React Native) | [Integrations SDK (mobile apps)](references/mobile.md) | diff --git a/skills/base44-platform-integrations-sdk/references/mobile.md b/skills/base44-platform-integrations-sdk/references/mobile.md new file mode 100644 index 0000000..a269d50 --- /dev/null +++ b/skills/base44-platform-integrations-sdk/references/mobile.md @@ -0,0 +1,185 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 integrations sdk: Integrations SDK (mobile apps) + +Integrations are packages of functions that are used to interact with external data / services. + +You can use integration packages from the pre-initialized base44 sdk client. +and call spefici integrations by with the relevant parameters. + +for example: +import { base44 } from "@/api/base44Client"; +const res = await base44.integrations.Core.InvokeLLM({ + prompt: "Give me data on Apple (the company)", + add_context_from_internet: true, + response_json_schema: { + type: "object", + properties: { + stock_price: {type: "number"}, + news_headlines: {type: "array", items: {type: "string"}}, + }, + } +}); + +InvokeLLM supports an optional `model` parameter for higher quality output on complex tasks (e.g. model: "claude-sonnet-5"). Non-default models use more integration credits, so only use them when the task requires it. When you use a non-default model, tell the user that it uses more integration credits. Only 'gemini_3_flash' and 'gemini_3_1_pro' support add_context_from_internet. Using an incompatible model will raise an error. + +<end_user_uploads> +Default every end-user upload to `UploadPrivateFile`. Use `UploadPublicFile` only when the user asked for these files to be public, or when something outside the app must fetch the file later without signing in. +</end_user_uploads> + +another example: +import { base44 } from "@/api/base44Client"; +const {file_uri} = await base44.integrations.Core.UploadPrivateFile({file: file}); + +`SendEmail` reaches registered app users normally. This app may ALSO email addresses that are not registered users, but only with a connected custom domain, on a paid plan, and subject to content screening, recipient-hygiene and anti-abuse limits that can refuse or pause sends — so never promise delivery to a non-registered address, and never route untrusted end-user input straight into the recipient. Invitations still use `base44.users.inviteUser`. + +Here are the integration packages available in this app: + +<integration_package> + <package_name>Core</package_name> + <package_description>Built-in integrations that are always available.</package_description> + <endpoints> + + <endpoint> + <name>InvokeLLM</name> + <description> + Generates a response from an LLM with a prompt. + Be elaborate and detailed in your prompt. + You can potentially ask the llm to output a json, in which case specify the json schema in response_json_schema. + If you need to use external knowledge, use add_context_from_internet=true. This will give the LLM context from google search, google maps, news, etc. That way you can fetch structured data from the web. + You can attach files (including images) for additional context to the LLM, using the `file_urls` parameter. This should be a list of urls after uploading them. + You can use both file_urls AND add_context_from_internet together - the system will use a model that supports both vision and web search. + You can optionally specify a `model` for this call. Options: 'automatic', 'gemini_3_8_flash', 'gpt_6_luna', 'claude-sonnet-5', 'claude_opus_5_5', 'gpt_6_sol', 'gpt_6_astra', 'claude_fable_5_1', 'glm_5_2'. Defaults to automatic. + 'gemini_3_8_flash' and 'gpt_6_luna' are fast; 'claude-sonnet-5' is balanced; 'claude_opus_5_5' and 'gpt_6_sol' are frontier models for hard reasoning; 'claude_fable_5_1' and 'gpt_6_astra' are the strongest and by far the most expensive. + All models except 'glm_5_2' support file_urls. Only 'gemini_3_8_flash' supports add_context_from_internet (web search). Using an incompatible model will raise an error. + Non-default models cost more integration credits. Only use them when the task requires higher quality. + If response_json_schema is specified, returns a dict (so no need to parse it), otherwise returns a string. + IMPORTANT: If specified, response_json_schema root object must by of type object, not array. i.e. {"type": "object", ...} + </description> + <input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'prompt': {'title': 'Prompt', 'type': 'string'}, 'add_context_from_internet': {'default': False, 'title': 'Add Context From Internet', 'type': 'boolean'}, 'response_json_schema': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'type': 'null'}], 'default': None, 'title': 'Response Json Schema'}, 'file_urls': {'anyOf': [{'format': 'binary', 'type': 'string'}, {'items': {'type': 'string'}, 'type': 'array'}, {'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'File Urls'}, 'model': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Model'}}, 'required': ['prompt'], 'title': 'InvokeLLMModel', 'type': 'object'}</input_schema> + </endpoint> + + <endpoint> + <name>TranscribeAudio</name> + <description> + Transcribes an audio file to text using speech recognition (Whisper). + First upload the audio file using the UploadPublicFile integration, then pass the resulting URL here. + Returns the transcript as a plain string. + Supported formats: ogg, oga, mp3, wav, webm, m4a, mp4, mpeg, mpga, flac. Maximum file size: 25MB. + </description> + <input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'audio_url': {'title': 'Audio Url', 'type': 'string'}}, 'required': ['audio_url'], 'title': 'TranscribeAudioModel', 'type': 'object'}</input_schema> + </endpoint> + + <endpoint> + <name>SendEmail</name> + <description>Sends email. Reaches registered app users always; reaching an address that is not a registered user requires a paid plan and a custom domain connected to the app. Optionally specify the sender name; otherwise the app name is used.</description> + <input_schema>{'$defs': {'EmailAttachment': {'description': 'One attachment as a caller supplies it: a name plus exactly one source.\n\nNo caller-supplied content type: the extension is already constrained to the\nallowlist, so it determines the type and the two can never disagree.', 'properties': {'filename': {'description': 'Name the recipient sees, with an extension from the supported set: csv, docx, gif, ics, jpeg, jpg, md, pdf, png, txt, webp, xlsx', 'title': 'Filename', 'type': 'string'}, 'file_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "URL or storage URI of a file THIS app already uploaded (e.g. the file_url returned by UploadFile). Another app's files are refused.", 'title': 'File Url'}, 'content': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Base64-encoded bytes, for a document the function generated itself. Supply either this or file_url, never both.', 'title': 'Content'}}, 'required': ['filename'], 'title': 'EmailAttachment', 'type': 'object'}}, 'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'from_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'From Name'}, 'to': {'title': 'To', 'type': 'string'}, 'subject': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Subject line. Optional with a template: it overrides the template's own <title>.", 'title': 'Subject'}, 'body': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Body'}, 'html': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "HTML body — the same thing as 'body' under an explicit name. Set one or the other, never both.", 'title': 'Html'}, 'text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Plain-text body. On its own, sends a plain-text email. Alongside 'body'/'html', both go out as one multipart/alternative email and the recipient's mail client renders whichever it prefers — so the two must say the same thing. Not accepted with a template: templates are HTML-only.", 'title': 'Text'}, 'template_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Id'}, 'template_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Name'}, 'variables': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'type': 'null'}], 'default': None, 'title': 'Variables'}, 'attachments': {'anyOf': [{'items': {'$ref': '#/$defs/EmailAttachment'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Attachments'}}, 'required': ['to'], 'title': 'SendEmailModel', 'type': 'object'}</input_schema> + </endpoint> + + <endpoint> + <name>SendPushNotification</name> + <description>Sends a native mobile push notification to one app user's phone. + + Use ONLY when the user explicitly asks for mobile push notifications and the + app ships as a native iOS/Android app. This does not reach the web app, and + it is not email or an in-app notification — for those use SendEmail or build + in-app UI instead. Never reach for it to "notify a user" in general. + + The app must have a native mobile build with push credentials configured; + without that the call fails at send time, so do not build a feature around it + unless the user confirmed they have a native mobile app. + + Server-side only: call it from a backend function via `asServiceRole` (a + logged-in end user must not be able to push to other users). + </description> + <input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'user_id': {'title': 'User Id', 'type': 'string'}, 'title': {'title': 'Title', 'type': 'string'}, 'content': {'title': 'Content', 'type': 'string'}, 'action_label': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Action Label'}, 'action_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Action Url'}}, 'required': ['user_id', 'title', 'content'], 'title': 'SendPushNotificationModel', 'type': 'object'}</input_schema> + </endpoint> + + <endpoint> + <name>UploadPublicFile</name> + <description> + Uploads a file to permanent, world-readable public storage. + Returns a dict containing {"file_url": string} + Anyone holding the url can fetch the file without signing in. + </description> + <input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'file': {'format': 'binary', 'title': 'File', 'type': 'string'}}, 'required': ['file'], 'title': 'UploadPublicFileModel', 'type': 'object'}</input_schema> + </endpoint> + + <endpoint> + <name>GenerateImage</name> + <description>Generate an image using AI. + Takes about 5-10 seconds to generate an image. + Be detailed in the prompt, the better the prompt, the better the image. + Optionally provide existing_image_urls as reference - for editing, style inspiration, or visual context. + The integration returns a URL to the generated image: {url: str}</description> + <input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'prompt': {'title': 'Prompt', 'type': 'string'}, 'existing_image_urls': {'anyOf': [{'items': {'type': 'string'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Existing Image Urls'}}, 'required': ['prompt'], 'title': 'GenerateImageModel', 'type': 'object'}</input_schema> + </endpoint> + + <endpoint> + <name>GenerateSpeech</name> + <description>Generate stored, shareable speech audio (TTS) from text. Returns an MP3 URL: {url: str}. + Multiple voices, consistent across users, high-quality multilingual (auto-detected if language_code is omitted). + For simple in-page playback, prefer the browser's TTS API. + Cost: 1 integration credit per 50 characters of text, up to 100 per call.</description> + <input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'text': {'description': 'Text to convert. Max 5000 chars.', 'title': 'Text', 'type': 'string'}, 'voice': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Optional voice (default 'river'):\n- river: calm, neutral.\n- honey: warm, soft.\n- sunny: bright, upbeat.\n- storm: formal, authoritative.\n- spark: energetic, quick.", 'title': 'Voice'}, 'language_code': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Optional ISO-639-1 code (e.g. 'en', 'es'). Omit to auto-detect.", 'title': 'Language Code'}}, 'required': ['text'], 'title': 'GenerateSpeechModel', 'type': 'object'}</input_schema> + </endpoint> + + <endpoint> + <name>GenerateVideo</name> + <description>Generate a video using AI. + + Takes about 30-60 seconds to generate a video using Google's Veo 3.x models. + Be detailed in the prompt - the better the prompt, the better the video. + + The integration returns a URL to the generated video: {url: str} + + Credit Cost: 5 integration credits per second of video + - 4 seconds = 20 credits + - 6 seconds (default) = 30 credits + - 8 seconds = 40 credits + </description> + <input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'prompt': {'title': 'Prompt', 'type': 'string'}, 'duration': {'default': 6, 'enum': [4, 6, 8], 'title': 'Duration', 'type': 'integer'}, 'aspect_ratio': {'default': '16:9', 'enum': ['16:9', '9:16'], 'title': 'Aspect Ratio', 'type': 'string'}, 'generate_audio': {'default': True, 'title': 'Generate Audio', 'type': 'boolean'}}, 'required': ['prompt'], 'title': 'GenerateVideoModel', 'type': 'object'}</input_schema> + </endpoint> + + <endpoint> + <name>ExtractDataFromUploadedFile</name> + <description> + Extracts data from an uploaded file. + Use the UploadPublicFile integration to get the file url first. + Supports csv, xlsx, json, html, png, jpg, jpeg, and pdf files. + Useful to extract data, and potential flow - upload file, extract data, bulk insert entities into database. + Only use this integration when you know the target JSON schema of the data you're trying to extract (if you just want the content of a file - fetch the file url). + returns the following object: + { + "status": "success" | "error", + "details": "error details if error, otherwise null", + "output": list[dict] | dict | None (exists if status is success) + } + </description> + <input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'file_url': {'title': 'File Url', 'type': 'string'}, 'json_schema': {'additionalProperties': True, 'title': 'Json Schema', 'type': 'object'}}, 'required': ['file_url', 'json_schema'], 'title': 'ExtractDataFromUploadedFileModel', 'type': 'object'}</input_schema> + </endpoint> + + <endpoint> + <name>CreateFileSignedUrl</name> + <description> + Creates a signed URL for a private file that can be used to download the file. + The file must be uploaded to the private storage. + Returns a dict containing {"signed_url": string} + </description> + <input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'file_uri': {'title': 'File Uri', 'type': 'string'}, 'expires_in': {'default': 300, 'title': 'Expires In', 'type': 'integer'}}, 'required': ['file_uri'], 'title': 'CreateFileSignedUrlModel', 'type': 'object'}</input_schema> + </endpoint> + + <endpoint> + <name>UploadPrivateFile</name> + <description> + Uploads a file to the app's private filhes directory. + Returns a dict containing {"file_uri": string} + You can use the file_uri later to create a signed URL for the file using the CreateFileSignedUrl integration + The file is uploaded to the private storage. + </description> + <input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'file': {'format': 'binary', 'title': 'File', 'type': 'string'}}, 'required': ['file'], 'title': 'UploadPrivateFileModel', 'type': 'object'}</input_schema> + </endpoint> + + </endpoints> +</integration_package> diff --git a/skills/base44-platform-integrations-sdk/references/web.md b/skills/base44-platform-integrations-sdk/references/web.md new file mode 100644 index 0000000..fadb4c2 --- /dev/null +++ b/skills/base44-platform-integrations-sdk/references/web.md @@ -0,0 +1,175 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 integrations sdk: Integrations and analytics SDK (web apps) + +Integrations: function packages for external data/services, on the pre-initialized base44 client: + +```js +const res = await base44.integrations.Core.InvokeLLM({ + prompt: "Give me data on Apple (the company)", + add_context_from_internet: true, + response_json_schema: {type: "object", properties: {stock_price: {type: "number"}}} +}); +const {file_uri} = await base44.integrations.Core.UploadPrivateFile({file: file}); +``` + +<end_user_uploads> +Default every end-user upload to `UploadPrivateFile`. Use `UploadPublicFile` only when the user asked for these files to be public, or when something outside the app must fetch the file later without signing in. +</end_user_uploads> +InvokeLLM optional `model` (e.g. model: "claude-sonnet-5") for complex tasks — non-default models use more integration credits: only when needed, and tell the user. add_context_from_internet only with 'gemini_3_flash'/'gemini_3_1_pro'; others raise an error. InvokeLLM is a single call with no tools — don't chain it to simulate an agent loop; agents are covered in the agents guide. + +`SendEmail` reaches registered app users normally. This app may ALSO email addresses that are not registered users, but only with a connected custom domain, on a paid plan, and subject to content screening, recipient-hygiene and anti-abuse limits that can refuse or pause sends — so never promise delivery to a non-registered address, and never route untrusted end-user input straight into the recipient. Invitations still use `base44.users.inviteUser`. + +Integration packages in this app: + +<integration_package> +<package_name>Core</package_name> +<package_description>Built-in integrations that are always available.</package_description> + +<endpoint> +<name>InvokeLLM</name> +<description> + Generates a response from an LLM with a prompt. + Be elaborate and detailed in your prompt. + You can potentially ask the llm to output a json, in which case specify the json schema in response_json_schema. + If you need to use external knowledge, use add_context_from_internet=true. This will give the LLM context from google search, google maps, news, etc. That way you can fetch structured data from the web. + You can attach files (including images) for additional context to the LLM, using the `file_urls` parameter. This should be a list of urls after uploading them. + You can use both file_urls AND add_context_from_internet together - the system will use a model that supports both vision and web search. + You can optionally specify a `model` for this call. Options: 'automatic', 'gemini_3_8_flash', 'gpt_6_luna', 'claude-sonnet-5', 'claude_opus_5_5', 'gpt_6_sol', 'gpt_6_astra', 'claude_fable_5_1', 'glm_5_2'. Defaults to automatic. + 'gemini_3_8_flash' and 'gpt_6_luna' are fast; 'claude-sonnet-5' is balanced; 'claude_opus_5_5' and 'gpt_6_sol' are frontier models for hard reasoning; 'claude_fable_5_1' and 'gpt_6_astra' are the strongest and by far the most expensive. + All models except 'glm_5_2' support file_urls. Only 'gemini_3_8_flash' supports add_context_from_internet (web search). Using an incompatible model will raise an error. + Non-default models cost more integration credits. Only use them when the task requires higher quality. + If response_json_schema is specified, returns a dict (so no need to parse it), otherwise returns a string. + IMPORTANT: If specified, response_json_schema root object must by of type object, not array. i.e. {"type": "object", ...} + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'prompt': {'title': 'Prompt', 'type': 'string'}, 'add_context_from_internet': {'default': False, 'title': 'Add Context From Internet', 'type': 'boolean'}, 'response_json_schema': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'type': 'null'}], 'default': None, 'title': 'Response Json Schema'}, 'file_urls': {'anyOf': [{'format': 'binary', 'type': 'string'}, {'items': {'type': 'string'}, 'type': 'array'}, {'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'File Urls'}, 'model': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Model'}}, 'required': ['prompt'], 'title': 'InvokeLLMModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>TranscribeAudio</name> +<description> + Transcribes an audio file to text using speech recognition (Whisper). + First upload the audio file using the UploadPublicFile integration, then pass the resulting URL here. + Returns the transcript as a plain string. + Supported formats: ogg, oga, mp3, wav, webm, m4a, mp4, mpeg, mpga, flac. Maximum file size: 25MB. + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'audio_url': {'title': 'Audio Url', 'type': 'string'}}, 'required': ['audio_url'], 'title': 'TranscribeAudioModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>SendEmail</name> +<description>Sends email. Reaches registered app users always; reaching an address that is not a registered user requires a paid plan and a custom domain connected to the app. Optionally specify the sender name; otherwise the app name is used.</description> +<input_schema>{'$defs': {'EmailAttachment': {'description': 'One attachment as a caller supplies it: a name plus exactly one source.\n\nNo caller-supplied content type: the extension is already constrained to the\nallowlist, so it determines the type and the two can never disagree.', 'properties': {'filename': {'description': 'Name the recipient sees, with an extension from the supported set: csv, docx, gif, ics, jpeg, jpg, md, pdf, png, txt, webp, xlsx', 'title': 'Filename', 'type': 'string'}, 'file_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "URL or storage URI of a file THIS app already uploaded (e.g. the file_url returned by UploadFile). Another app's files are refused.", 'title': 'File Url'}, 'content': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Base64-encoded bytes, for a document the function generated itself. Supply either this or file_url, never both.', 'title': 'Content'}}, 'required': ['filename'], 'title': 'EmailAttachment', 'type': 'object'}}, 'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'from_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'From Name'}, 'to': {'title': 'To', 'type': 'string'}, 'subject': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Subject line. Optional with a template: it overrides the template's own <title>.", 'title': 'Subject'}, 'body': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Body'}, 'html': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "HTML body — the same thing as 'body' under an explicit name. Set one or the other, never both.", 'title': 'Html'}, 'text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Plain-text body. On its own, sends a plain-text email. Alongside 'body'/'html', both go out as one multipart/alternative email and the recipient's mail client renders whichever it prefers — so the two must say the same thing. Not accepted with a template: templates are HTML-only.", 'title': 'Text'}, 'template_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Id'}, 'template_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Name'}, 'variables': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'type': 'null'}], 'default': None, 'title': 'Variables'}, 'attachments': {'anyOf': [{'items': {'$ref': '#/$defs/EmailAttachment'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Attachments'}}, 'required': ['to'], 'title': 'SendEmailModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>SendPushNotification</name> +<description>Sends a native mobile push notification to one app user's phone. + + Use ONLY when the user explicitly asks for mobile push notifications and the + app ships as a native iOS/Android app. This does not reach the web app, and + it is not email or an in-app notification — for those use SendEmail or build + in-app UI instead. Never reach for it to "notify a user" in general. + + The app must have a native mobile build with push credentials configured; + without that the call fails at send time, so do not build a feature around it + unless the user confirmed they have a native mobile app. + + Server-side only: call it from a backend function via `asServiceRole` (a + logged-in end user must not be able to push to other users). + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'user_id': {'title': 'User Id', 'type': 'string'}, 'title': {'title': 'Title', 'type': 'string'}, 'content': {'title': 'Content', 'type': 'string'}, 'action_label': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Action Label'}, 'action_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Action Url'}}, 'required': ['user_id', 'title', 'content'], 'title': 'SendPushNotificationModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>UploadPublicFile</name> +<description> + Uploads a file to permanent, world-readable public storage. + Returns a dict containing {"file_url": string} + Anyone holding the url can fetch the file without signing in. + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'file': {'format': 'binary', 'title': 'File', 'type': 'string'}}, 'required': ['file'], 'title': 'UploadPublicFileModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>GenerateImage</name> +<description>Generate an image using AI. + Takes about 5-10 seconds to generate an image. + Be detailed in the prompt, the better the prompt, the better the image. + Optionally provide existing_image_urls as reference - for editing, style inspiration, or visual context. + The integration returns a URL to the generated image: {url: str}</description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'prompt': {'title': 'Prompt', 'type': 'string'}, 'existing_image_urls': {'anyOf': [{'items': {'type': 'string'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Existing Image Urls'}}, 'required': ['prompt'], 'title': 'GenerateImageModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>GenerateSpeech</name> +<description>Generate stored, shareable speech audio (TTS) from text. Returns an MP3 URL: {url: str}. + Multiple voices, consistent across users, high-quality multilingual (auto-detected if language_code is omitted). + For simple in-page playback, prefer the browser's TTS API. + Cost: 1 integration credit per 50 characters of text, up to 100 per call.</description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'text': {'description': 'Text to convert. Max 5000 chars.', 'title': 'Text', 'type': 'string'}, 'voice': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Optional voice (default 'river'):\n- river: calm, neutral.\n- honey: warm, soft.\n- sunny: bright, upbeat.\n- storm: formal, authoritative.\n- spark: energetic, quick.", 'title': 'Voice'}, 'language_code': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Optional ISO-639-1 code (e.g. 'en', 'es'). Omit to auto-detect.", 'title': 'Language Code'}}, 'required': ['text'], 'title': 'GenerateSpeechModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>GenerateVideo</name> +<description>Generate a video using AI. + + Takes about 30-60 seconds to generate a video using Google's Veo 3.x models. + Be detailed in the prompt - the better the prompt, the better the video. + + The integration returns a URL to the generated video: {url: str} + + Credit Cost: 5 integration credits per second of video + - 4 seconds = 20 credits + - 6 seconds (default) = 30 credits + - 8 seconds = 40 credits + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'prompt': {'title': 'Prompt', 'type': 'string'}, 'duration': {'default': 6, 'enum': [4, 6, 8], 'title': 'Duration', 'type': 'integer'}, 'aspect_ratio': {'default': '16:9', 'enum': ['16:9', '9:16'], 'title': 'Aspect Ratio', 'type': 'string'}, 'generate_audio': {'default': True, 'title': 'Generate Audio', 'type': 'boolean'}}, 'required': ['prompt'], 'title': 'GenerateVideoModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>ExtractDataFromUploadedFile</name> +<description> + Extracts data from an uploaded file. + Use the UploadPublicFile integration to get the file url first. + Supports csv, xlsx, json, html, png, jpg, jpeg, and pdf files. + Useful to extract data, and potential flow - upload file, extract data, bulk insert entities into database. + Only use this integration when you know the target JSON schema of the data you're trying to extract (if you just want the content of a file - fetch the file url). + returns the following object: + { + "status": "success" | "error", + "details": "error details if error, otherwise null", + "output": list[dict] | dict | None (exists if status is success) + } + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'file_url': {'title': 'File Url', 'type': 'string'}, 'json_schema': {'additionalProperties': True, 'title': 'Json Schema', 'type': 'object'}}, 'required': ['file_url', 'json_schema'], 'title': 'ExtractDataFromUploadedFileModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>CreateFileSignedUrl</name> +<description> + Creates a signed URL for a private file that can be used to download the file. + The file must be uploaded to the private storage. + Returns a dict containing {"signed_url": string} + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'file_uri': {'title': 'File Uri', 'type': 'string'}, 'expires_in': {'default': 300, 'title': 'Expires In', 'type': 'integer'}}, 'required': ['file_uri'], 'title': 'CreateFileSignedUrlModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>UploadPrivateFile</name> +<description> + Uploads a file to the app's private filhes directory. + Returns a dict containing {"file_uri": string} + You can use the file_uri later to create a signed URL for the file using the CreateFileSignedUrl integration + The file is uploaded to the private storage. + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'file': {'format': 'binary', 'title': 'File', 'type': 'string'}}, 'required': ['file'], 'title': 'UploadPrivateFileModel', 'type': 'object'}</input_schema> +</endpoint> + +</integration_package> + +Track custom events with base44 from '@/api/base44Client': +base44.analytics.track({ eventName: string, properties?: Record<string, string | number | boolean | null> }) +Indicative event names (e.g. "user_contact_form_submit"), minimal properties, no PII. +To opt-out, set `{"analytics":{"enabled":false}}` when initializing the base44 SDK (supported from 0.8.46). diff --git a/skills/base44-platform-manifest.json b/skills/base44-platform-manifest.json new file mode 100644 index 0000000..3d8cfa8 --- /dev/null +++ b/skills/base44-platform-manifest.json @@ -0,0 +1,82 @@ +{ + "files": { + "skills/base44-platform-actors/SKILL.md": "a8906d6a448905f839631b80e391f9bd3a982391cc69c181e1f5818888881afd", + "skills/base44-platform-actors/references/mobile.md": "a64520a0d9c6277e2c3289536d8c80f240efbf996589bf463153e78dbf6d0e5f", + "skills/base44-platform-actors/references/web.md": "7a4e32c4911952225c7d28d85479c480ccee5df72ce99d7fcede7dcb64d3eb0c", + "skills/base44-platform-agents/SKILL.md": "a426489d2e27dcc78e5dea84aa8f3a7be9dc79563559f3d68328cd9533ad5098", + "skills/base44-platform-agents/references/web-spa.md": "b569d56379dca260f7ab8e0ca767330031ea329e42a2f395c3c5269db372d7ec", + "skills/base44-platform-agents/references/web-tanstack.md": "ce8257bdc0b5c316734f143115f3a7dda8ba8358a61a73c00b4ebd1bc72481a9", + "skills/base44-platform-backend-functions/SKILL.md": "66e96aec70ce4303462465a0baf9f46342b7e60c70267b1c4b23c38b39e31fc6", + "skills/base44-platform-backend-functions/references/mobile.md": "a98e72be7fdc1c1b511b594648ae1937e5bfb46ff72350fb6c6cd044b2304556", + "skills/base44-platform-backend-functions/references/web-spa.md": "b62f5ffd2ed52fb1b8d5dfedb2e70a2138d3ce30b0aee1023d433df1c6fe1c76", + "skills/base44-platform-backend-functions/references/web-tanstack.md": "0b7f4fb263e82a7c22546cadbf54bd959fa88da9fa4da79bacd467bd037f52b9", + "skills/base44-platform-connectors/SKILL.md": "4bb82de0cabd141953434d553a1f1c57754ea07163651ec383ea5f0edb825046", + "skills/base44-platform-connectors/references/mobile.md": "a8640d9156acbe12bc89599da1d91f2985fabdd2970c6224c557dfb2219e4b9c", + "skills/base44-platform-connectors/references/web-spa.md": "ea850bc48c003b7c79ac51dec60dca2b942be1328e177298027dcf464f37ba60", + "skills/base44-platform-connectors/references/web-tanstack.md": "4576eba68f373ca136b8087cd820c29e8771b16bdcc8011e26eb9f74db9ee487", + "skills/base44-platform-entities/SKILL.md": "c84de4b41d8d64b20e4a4522a97422f500eb3554e9ccc48075130fdb8ae90d06", + "skills/base44-platform-entities/references/mobile-and-games.md": "559313916e24dcfb4cc9ba2e250d8c449ac8555ce8e5494d91d8075b86aa1b0d", + "skills/base44-platform-entities/references/rls.md": "d70c3f6aee3f9d4bb4a771078cd70e1c2ddfffa84a4598627b36c13f490e77b0", + "skills/base44-platform-entities/references/web.md": "bd288ee846dedd4e908f93a03291a12a4d26dc4877db46c92d05758d7ef15934", + "skills/base44-platform-frontend/SKILL.md": "40f9751943bcaac870313afa6b6335bd9d05908a7cee1f82be454f2ada38f8f0", + "skills/base44-platform-frontend/references/mobile.md": "68d1983605b40f812ee6ed6374649763f469681fd8cfd5f198a3e39e26107957", + "skills/base44-platform-frontend/references/web-spa.md": "a019c49df78cbd7f4b458992fc2a6483f10230c8a2f475dc34c035b3af38ae3e", + "skills/base44-platform-frontend/references/web-tanstack.md": "8962af6b15fc2f0cd39a0822189f87eb5558a2bb6809412bf9ac89cd4b9d7df7", + "skills/base44-platform-games/SKILL.md": "a41d9aae095b377e17c35e207b014718f1b674ccf766330d6638dd63b4d92a1c", + "skills/base44-platform-games/references/game-2d.md": "b07667d58c77348b181b45a2d202836362d71d2e21db5569aefa107c5e1b417d", + "skills/base44-platform-games/references/game-3d.md": "6dff6bfc6fb221c0e39fb4517fc230b3c68265e6b25d700c4e7c29fcea6a8946", + "skills/base44-platform-games/references/phaser/actions_and_utilities.md": "0e9b79f431c60f34535de6ed1098162ee9aebc9838a6fc077d2b0144bf66a26c", + "skills/base44-platform-games/references/phaser/animations.md": "2a7bae86a72abd200717da0a6d3022af5226c0701f93e16a50909ed2b516d4d5", + "skills/base44-platform-games/references/phaser/audio_and_sound.md": "0f121856952f0288712b0dd83404a461e855f6cdd590884e2151506b6689b4f3", + "skills/base44-platform-games/references/phaser/cameras.md": "17ba29369c4f42afb0a6594af6a11fdb28e36f47b5c79de3343fb68092c4251c", + "skills/base44-platform-games/references/phaser/curves_and_paths.md": "b59ff3f894e705818598fc3ddfa824f485317fbd1acd08cbdff4b325719b274a", + "skills/base44-platform-games/references/phaser/data_manager.md": "3312a26a0be196c8360484204b37905bea783bf86d08ac309b00e929d6886155", + "skills/base44-platform-games/references/phaser/events_system.md": "f376277861973731e778d64d0e3202238aebfb3e91f2e7cccf9024c3ac144eb1", + "skills/base44-platform-games/references/phaser/filters_and_postfx.md": "6f826aeb18359a9de309e1c5855e70d76127d07bad84596cd7a0f8cb9ab5d882", + "skills/base44-platform-games/references/phaser/game_object_components.md": "d96c5b50b23b9255fa3090800af993cf5241dc18207604a6122b0011f4b016ca", + "skills/base44-platform-games/references/phaser/game_setup_and_config.md": "b012a4362069c7cdae769d4c3644f987271cdc867b1848ab714fb6743a24d590", + "skills/base44-platform-games/references/phaser/geometry_and_math.md": "5eb35bc5f7d202a5e4779e27c44b88eeb5f769482617e16cb8b85f0bb3ceb614", + "skills/base44-platform-games/references/phaser/graphics_and_shapes.md": "8962622df9c62ae04651f76351aaeaa4e1cd7ab1b227ee55ee09baa1a4d54519", + "skills/base44-platform-games/references/phaser/groups_and_containers.md": "184a8b901dca19b7c884ab186fec2ffc11e0486c6a361687f20e039673b32152", + "skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md": "d89eeaff3f53ef71851dd2e5ef377aea25004cd904ddcf316a67ac5cc6a56c9b", + "skills/base44-platform-games/references/phaser/loading_assets.md": "089cbbfe2d0ecdbcd32cd968f5e7679a92bcdd739ee1f010c9e409f1b7221b75", + "skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md": "805bbe67ccbc8b6aeac4c8b2350f0e37a916b871205374e40c9fa6678b60c57a", + "skills/base44-platform-games/references/phaser/particles.md": "f54ab6d5affc3dee8b96bb0cc972ef519827c115007b89ff4eed7105ab5f137c", + "skills/base44-platform-games/references/phaser/physics_arcade.md": "aaf41916ce2360b847da4817a4b9d11b79c99ede2facba2f2e0601817f402992", + "skills/base44-platform-games/references/phaser/physics_matter.md": "5710fd82ad60658ba63b9084f747a448940efc5792b7f76601740b5a3c74ccb1", + "skills/base44-platform-games/references/phaser/render_textures.md": "82783870d125fd1f47927a11cfda853275e7c4458183caecce5cd5783233643f", + "skills/base44-platform-games/references/phaser/scale_and_responsive.md": "f4d60bfa9194b4d1c9cac12478d198047ea2747c5b29b8eea8b1eeefca682f8f", + "skills/base44-platform-games/references/phaser/scenes.md": "c0ae75d7013d275ea17d465a36b710eae54dffec988bf10648c5938cb3d644c8", + "skills/base44-platform-games/references/phaser/sprites_and_images.md": "ae6dfb48e6f37932a24da09723246552e27ef68e353fe521e95c186622e9075d", + "skills/base44-platform-games/references/phaser/text_and_bitmaptext.md": "44aa65d76bd320756957a88cb0c2f8487fc4b65decb0c63eb23a1b7f294fdb73", + "skills/base44-platform-games/references/phaser/tilemaps.md": "a29644f606baa35b0043743a837c01fbff74e70b26ce8aef0f16116b22f4ec2d", + "skills/base44-platform-games/references/phaser/time_and_timers.md": "662e293a3da5a6bf161036062488c1e61f55df4b045ef0fca43c8caf89c84469", + "skills/base44-platform-games/references/phaser/tweens.md": "ebd1132bd8525f3235763a79a4864c324fdab768698cc91553eb4ee9d49ef5f4", + "skills/base44-platform-games/references/threejs/assets_models_textures.md": "7054454e4483b511a1fcae96a66b80f8274af3277fd5dbc21c546bd9cfba0499", + "skills/base44-platform-games/references/threejs/cameras.md": "03f3f3b2ff4d557fac36c3b5016932dfb769195ff96ba02d8831bfa979a9fd89", + "skills/base44-platform-games/references/threejs/character_controller.md": "742affe45cba7580f146e65d9b558234986bdadee13824d0abd726649855a2c3", + "skills/base44-platform-games/references/threejs/enemies_and_movement.md": "8e0a6029e1f889e6f73e276650c80470319726abefaf68bee3787828d64f57b6", + "skills/base44-platform-games/references/threejs/game_state_and_flow.md": "4a8ba35f9f1b146f574236d095ee4aa3ed3b75e729fad0dd96405c363098b5c9", + "skills/base44-platform-games/references/threejs/input_and_controls.md": "012e6a4f94c2b3a9f41f33101bcc02a01e836102b4a49800bdccba9903c3ce7a", + "skills/base44-platform-games/references/threejs/performance.md": "dce5ea18f566aabccaa6d2a4ceb7acff4c09e15328fbce2f213f55cadf07922d", + "skills/base44-platform-games/references/threejs/physics_dynamics.md": "6c6b413ebb61de82dce332d552e2ecc4a8f1cf9c55be0a7cc25b1613cd5f071a", + "skills/base44-platform-games/references/threejs/projectiles_and_pooling.md": "d6347da652522fa9d787b9aacc61e298d3347dd8b21b47880057ed2abc5cf597", + "skills/base44-platform-games/references/threejs/scene_and_rendering.md": "04b3c9302c85d51342ff2c176f375b3303cddf0e1cfff105e33e29d34a32c236", + "skills/base44-platform-games/references/threejs/text_hud_ui.md": "4e3f879b4f4f06eb035a2d9cc8d709e66d2178114018c5107e55c883a53a9de5", + "skills/base44-platform-games/references/threejs/triggers_and_collisions.md": "09d686d06ba9a85310250b61113e7c57883a184ae14458899f0458f39cb80be5", + "skills/base44-platform-games/references/threejs/vfx_and_juice.md": "c69797aa5d45def521138729eb08ab8b369c0517371b29abe49637381a39729c", + "skills/base44-platform-integrations-sdk/SKILL.md": "b126e9ac424dea008702e2cd54925cfeb76cdbbe9fda31f3ac2c3c043d7c9f92", + "skills/base44-platform-integrations-sdk/references/mobile.md": "57575c8c7ff975add232955b384cb106d81e302646929c7a652176d49723c82c", + "skills/base44-platform-integrations-sdk/references/web.md": "0e38e2cfee66514099b67e201af86284b1ed852bc145df67bce7bb15000048f9", + "skills/base44-platform-platform/SKILL.md": "244f32d1afeb6a730f1bede1d266d59178cf746c98f5f8d25cb71d4aefa866a7", + "skills/base44-platform-platform/references/game.md": "2ad9fe16f5286582db4ca2c9a201658d4970fae327db4240ae5ad2316ed043bc", + "skills/base44-platform-platform/references/mobile.md": "e6a2cc30e2ea4b355242ddcf21316168df8e71d57c26ee8124d1d08d51c815ad", + "skills/base44-platform-platform/references/web-spa.md": "d1c36c3b8604f5ffcfc45f3ba2427320a3d45aaa4903b341d0825a1a9ce41711", + "skills/base44-platform-platform/references/web-tanstack.md": "847c72381358b9dba8607835d7d46fcf955fae65af9f73d87baa7b74ae7c390a", + "skills/base44-platform-workflows/SKILL.md": "55fb8de2004bbbcb4369ac58b91acd5613c8b750eed79e4dcc7b657e78891284", + "skills/base44-platform-workflows/references/automations-legacy.md": "df52bfdc18a3e7b886c169224edb68479771efd2f71eebd2652f43482caf10bc", + "skills/base44-platform-workflows/references/mobile-automations.md": "7ab2c859ac0ad6f531ae0918a7bf125e5611af76e7905693c0f78c8e2c83799e", + "skills/base44-platform-workflows/references/web-spa.md": "d4bed7815f168037a52626e3fb83837bf530914398b642349f75c2e0193c06a3", + "skills/base44-platform-workflows/references/web-tanstack.md": "b9c50670cbd7bf27eb184796a330f7603b5a92cebbbba386f2fef792f6961ec2" + } +} diff --git a/skills/base44-platform-platform/SKILL.md b/skills/base44-platform-platform/SKILL.md new file mode 100644 index 0000000..c66594a --- /dev/null +++ b/skills/base44-platform-platform/SKILL.md @@ -0,0 +1,26 @@ +--- +name: base44-platform-platform +description: "Base44 platform concepts for a web app: entities, backend functions or server code, workflows, pages and layouts, the project file structure, authentication (platform login vs the custom-auth boilerplate), user invites and the GitHub features. Read first when starting work on a Base44 web app." +--- +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 platform + +A Base44 web app is a Vite + React SPA or a TanStack Start project backed by Base44's entities, functions and auth. This reference is the platform orientation the builder itself works from: concepts, file layout, authentication wiring and user invites. + +## Pick the reference for this app + +Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (over the Base44 MCP `execute_api` +tool, or with your Base44 CLI token); it returns the framework, app type, runtime, +entitlements and the exact reference files to read. Without it, detect from the project files: +`src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a +native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. + +| Profile | Read | +|---|---| +| Web app, Vite SPA (the default web template) | [Web app, Vite SPA](references/web-spa.md) | +| Web app, TanStack Start | [Web app, TanStack Start](references/web-tanstack.md) | +| Native mobile app (Expo, React Native) | [Native mobile app (Expo)](references/mobile.md) | +| Game, 2D (Phaser 4) | [Game (Phaser 2D or Three.js 3D)](references/game.md) | +| Game, 3D (Three.js) | [Game (Phaser 2D or Three.js 3D)](references/game.md) | diff --git a/skills/base44-platform-platform/references/game.md b/skills/base44-platform-platform/references/game.md new file mode 100644 index 0000000..1be918a --- /dev/null +++ b/skills/base44-platform-platform/references/game.md @@ -0,0 +1,20 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 platform: Game (Phaser 2D or Three.js 3D) + +<technology_stack> +Base44 games are built on React + Tailwind CSS, inside Base44's React shell. 2D games run on Phaser 4; 3D games run on Three.js through React Three Fiber (no Phaser). Other frameworks (Next.js, Python, etc.) are not supported. Do NOT combine Phaser and Three.js in one game unless the user explicitly asks for it. +Games are using Base44's backend as a service - that gives them data management, integrations, and more. + +2D vs 3D is settled when the game is created; the engine in use is in the app profile (`game_engine`). Converting an existing game between 2D and 3D is a builder operation: ask the user to switch the engine in the Base44 builder, then rebuild in the new engine. + +The React overlay always renders ABOVE the game canvas — Phaser or Three.js. A HUD button that is visible but not clickable is missing `pointer-events-auto` (or the canvas is stacked above it). +</technology_stack> + +<backend> +- entities: entities are defined as json schema objects that define the shape of the data that is stored in the app. +- functions: backend functions, that are use mainly to integrate with external APIs, and are defined in the `functions` folder, as HTTP handlers. +- automations: automations run backend functions automatically. Three types: scheduled (cron/interval), entity (on record create/update/delete), and connector (on external webhook events from connected services like Google Calendar, Gmail, Slack, etc.). +- `base44/` is server-side and never in the client bundle: never import it from a frontend file (the game calls `base44.functions.invoke(...)` instead), and never import `src/` or `@/` from a function. +</backend> diff --git a/skills/base44-platform-platform/references/mobile.md b/skills/base44-platform-platform/references/mobile.md new file mode 100644 index 0000000..6da08ff --- /dev/null +++ b/skills/base44-platform-platform/references/mobile.md @@ -0,0 +1,58 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 platform: Native mobile app (Expo) + +Here's a list of concepts you can use to develop the app: +<backend> +- entities: entities are defined as json schema objects that define the shape of the data that is stored in the app. +- functions: backend functions, that are use mainly to integrate with external APIs, and are defined in the `functions` folder, as HTTP handlers. +- automations: automations run backend functions automatically. Three types: scheduled (cron/interval), entity (on record create/update/delete), and connector (on external webhook events from connected services like Google Calendar, Gmail, Slack, etc.). + +</backend> +<frontend> +- screens: screens are defined as React Native components in the `app/` directory — each file is automatically a route (expo-router file-based navigation). +- components: components are defined as React Native components, that can be used on screens. +- root layout: `app/_layout.jsx` wraps every screen (navigation stack, providers). +</frontend> + +<file_structure> +This is an Expo + React Native project using expo-router (file-based routing). + +Standard directories: +- app/... (jsx files) — every file is a screen/route (e.g., app/index.jsx is the home screen, app/settings.jsx → /settings, app/items/[id].jsx → /items/:id) +- app/_layout.jsx — root layout (Stack navigator, providers, global.css import) +- src/components/... (jsx files) — reusable components, can have subfolders +- base44/entities/<Name>.jsonc — entity JSON schema files +- base44/functions/<name>/entry.ts — backend functions +- tailwind.config.js (Tailwind/NativeWind theme configuration) +- global.css (Tailwind directives) + +You are free to create additional directories and files as needed (e.g., src/lib/, src/hooks/, src/context/, src/utils/). +IMPORTANT: In code, import shared code with the @/ alias (e.g., import TaskCard from "@/components/TaskCard") — the alias maps to src/. Screen files in app/ are routes, never import them manually. + +Create, edit and delete files directly at these paths; a change is live once the file is written. +</file_structure> + +<authentication> +Authentication backend (token issuance, sessions, email verification) is handled by the base44 platform. +You do NOT need to implement any auth backend logic. + +Login and authentication are handled by the base44 platform, +you don't need to worry about them nor to implement them. +Therefore, IMPORTANT: NEVER try to create a login page. +</authentication> + +<invite_user> +You can use the `base44.users.inviteUser` function from base44 sdk in order to invite users to the app. +It accepts email and role in the app ("user" or "admin"). Make sure you understand the role that the builder is aiming for. + +Since the app is public, both admin users and regular users can invite others. However, only admin users can invite with role="admin", while regular users can only invite with role="user". Pay attention where you locate the call to this function, otherwise it may not work as expected. + +Example: +```js +import { base44 } from '@/api/base44Client'; + +await base44.users.inviteUser("foo@gmail.com", "user") +``` +</invite_user> diff --git a/skills/base44-platform-platform/references/web-spa.md b/skills/base44-platform-platform/references/web-spa.md new file mode 100644 index 0000000..cbdd316 --- /dev/null +++ b/skills/base44-platform-platform/references/web-spa.md @@ -0,0 +1,70 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 platform: Web app, Vite SPA + +<concepts> +Backend: entities (JSON schemas defining stored data); functions (external-API integration, HTTP handlers in `base44/functions/{functionName}/entry.ts`); workflows (multi-step processes — triggers/conditions/delays/branching — as `.jsonc` in `base44/workflows/`). +Frontend: pages (React components composing the app), components, layout (wraps every page). +</concepts> + +<file_structure> +Standard Vite + React project; create files anywhere. + +- src/pages/... (jsx, subfolders ok) · src/components/... (jsx, subfolders ok) · base44/entities/... (jsonc) · base44/agents/... (jsonc) · base44/functions/{functionName}/entry.ts · base44/workflows/... (jsonc) +- src/App.jsx (router) · index.html · src/index.css (design tokens) + tailwind.config.js +- Extra dirs as needed (src/lib/, src/hooks/, …). Frontend files use src/ paths; entities/agents/functions/workflows use their base44/… paths above; code imports use the @/ alias (never relative src/ paths — they break on moves). + +Create, edit and delete files directly at these paths; a change is live once the file is written. +</file_structure> + +<authentication> +The platform owns the auth backend (tokens, sessions, email verification) — never implement auth backend logic. +Whether this app is enrolled in custom auth is in the app profile (`custom_auth_enrolled`). Not enrolled: the platform provides the login experience — never create a login page. Enrolled: the rest of this section applies. + +Boilerplate auth pages already exist on disk — never create, recreate, make parallel versions of, or preemptively rewrite them. If one seems missing from your context, assume it still exists; read the file to confirm before writing one: +`src/pages/Login.jsx` (<Login />) · `src/pages/Register.jsx` · `src/pages/ForgotPassword.jsx` · `src/pages/ResetPassword.jsx` (reads `?token=`). Fully functional: flows, Google OAuth, OTP, reset, errors, redirects. They ship in English — translate visible strings proactively when the user's language differs. + +Seeding guarantees the page files, NOT their routes — `src/App.jsx` may or may not have them registered yet. As soon as the app gets any auth surface — a login/sign-up entry point, an admin/management/dashboard screen, a `ProtectedRoute`-gated page, or anything that redirects to login — all four routes below must be registered in `src/App.jsx`: keep every one already there, ADD the missing ones. All four go in together even when only one of them was asked for, because each page links to the others and the reset email targets `/reset-password`. Never remove or rewrite an auth route that already exists. Gate authenticated pages by nesting them under `ProtectedRoute` as a layout route. Use these literal paths: +```javascript +import ProtectedRoute from '@/components/ProtectedRoute'; +<Routes> + <Route path="/login" element={<Login />} /> + <Route path="/register" element={<Register />} /> + <Route path="/forgot-password" element={<ForgotPassword />} /> + <Route path="/reset-password" element={<ResetPassword />} /> + <Route element={<ProtectedRoute unauthenticatedElement={<Navigate to="/login" replace />} />}> + <Route path="/" element={<Home />} /> + </Route> + <Route path="*" element={<PageNotFound />} /> +</Routes> +``` +ProtectedRoute lives at `src/components/ProtectedRoute.jsx` (props: fallback, unauthenticatedElement; renders <Outlet /> when authed). Recreate at same path/name if deleted. + +Customize auth pages only on explicit request; edit in place; preserve every flow step (register → OTP stays multi-step in any layout — never shortcut OTP with loginViaEmailPassword). + +SDK (for template edits): +- loginViaEmailPassword(email, password) — sets token; then hard redirect to the boilerplate's post-login destination — it resolves a same-origin `returnTo` query param (falling back to `'/'`); preserve that handling when editing, never hardcode a bare `'/'` +- loginWithProvider(provider, fromUrl) — "google"|"facebook"|"microsoft"|"apple"; pass post-login dest as fromUrl (the resolved `returnTo`, else e.g. createPageUrl("Home")); redirects +- register({email, password}) — does NOT log in; user unverified. Required: register → OTP → verifyOtp → setToken(access_token) → hard redirect. Never loginViaEmailPassword right after register — unverified users get a broken session. +- verifyOtp({email, otpCode}) — returns access_token → setToken → hard redirect +- resendOtp(email) +- resetPasswordRequest(email) — always show generic success (API hides whether email exists) +- resetPassword({resetToken, newPassword}) — then hard-redirect to login route if it exists, else `/` +- isAuthenticated() → Promise<boolean> · logout(redirectUrl?) +Hard redirects (window.location.href), never navigate() — the auth provider must re-initialize. Auth calls throw: keep the templates' try/catch + inline errors + disabled submit while in-flight (exception: resetPasswordRequest always shows generic success). + +Missing template — only after reading the directory confirms it is actually absent (e.g. a deleted/legacy app, often surfaced by a `Cannot resolve '@/pages/Login'` build error — but a build error alone never justifies recreation; check first): RECREATE it at the exact path/name per the SDK rules above — never "fix" by removing the import/route or substituting something simpler. Login: email+password, Google button, forgot link. Register: email+password+confirm, Google, register→OTP→verifyOtp flow, resend. ForgotPassword: email → resetPasswordRequest. ResetPassword: `?token=` + new password+confirm → resetPassword. + +</authentication> + +<github_integration> +Two GitHub features exist: 2-way repo sync, and a GitHub connector (build features on GitHub data). On "GitHub", ask which one before proceeding. +</github_integration> + +<invite_user> +`await base44.users.inviteUser("foo@gmail.com", "user")` invites a user — email + app role "user" or "admin"; confirm which role the builder wants. + +Public app: admins invite both roles; regular users only role="user". Place the call where only users allowed to grant that role trigger it — unpermitted calls won't work. + +</invite_user> diff --git a/skills/base44-platform-platform/references/web-tanstack.md b/skills/base44-platform-platform/references/web-tanstack.md new file mode 100644 index 0000000..04e0f06 --- /dev/null +++ b/skills/base44-platform-platform/references/web-tanstack.md @@ -0,0 +1,70 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 platform: Web app, TanStack Start + +<concepts> +Backend: entities (JSON schemas defining stored data); server functions and server routes (this app's own server code, see <server_code>). +Frontend: routes (one React component per file in `src/routes/`, server-rendered by default), components, layouts (route files that render `<Outlet />`). +</concepts> + +<file_structure> +TanStack Start project (Vite, built for Cloudflare Workers — never remove or change the plugins in `vite.config.js`); create files anywhere under src/. + +- src/routes/... (jsx — one file per route, see <file_routes>) · src/routes/api/... (js — server routes) · src/lib/server-fns.js (server functions) · src/lib/*.server.js (server-only code) · src/components/... (jsx, subfolders ok) · base44/entities/... (jsonc) · base44/agents/... (jsonc) +- src/routes/__root.jsx (the HTML document + providers) · src/index.css (design tokens) + tailwind.config.js · src/router.jsx · src/start.js · src/routeTree.gen.ts (generated — never edit) +- Extra dirs as needed (src/lib/, src/hooks/, …). Frontend files use src/ paths; entities/agents use their base44/… paths above; code imports use the @/ alias (never relative src/ paths — they break on moves). + +Create, edit and delete files directly at these paths; a change is live once the file is written. +</file_structure> + +<authentication> +The platform owns the auth backend (tokens, sessions, email verification) — never implement auth backend logic. +Whether this app is enrolled in custom auth is in the app profile (`custom_auth_enrolled`). Not enrolled: the platform provides the login experience — never create a login page. Enrolled: the rest of this section applies. + +Boilerplate auth components already exist in `src/components/auth/` — never create, recreate, make parallel versions of, or preemptively rewrite them. If one seems missing from your context, assume it still exists; read the file to confirm before writing one: +`LoginPage` · `RegisterPage` (register → email code → verifyOtp) · `ForgotPasswordPage` · `ResetPasswordPage` (reads `?token=`) · `AuthGate.jsx` (`requireSignedIn`, `AuthGateError`), each in `src/components/auth/<Name>.jsx`, with the shared `AuthLayout`. Fully functional: flows, Google OAuth, OTP, reset, errors, redirects. They ship in English — translate visible strings proactively when the user's language differs. + +The boilerplate guarantees the components, not their routes — the starter has no auth routes, and a file under `src/routes/` is live as soon as it exists. As soon as the app gets any auth surface — a login/sign-up entry point, an admin/management/dashboard screen, a gated page, or anything that sends visitors to login — all four route files below must exist: keep every one already there, add the missing ones. All four go in together even when only one of them was asked for, because each page links to the others and the reset email targets `/reset-password`. Never remove or rewrite an auth route that already exists. Each only wires its component: +```javascript +// src/routes/login.jsx — register.jsx (RegisterPage), forgot-password.jsx (ForgotPasswordPage) and reset-password.jsx (ResetPasswordPage) follow the same shape +import { createFileRoute } from "@tanstack/react-router"; +import { LoginPage } from "@/components/auth/LoginPage"; +export const Route = createFileRoute("/login")({ ssr: false, component: LoginPage }); +``` +Gate authenticated pages with one pathless layout route and put each gated page under `src/routes/_authed/` (`_authed/dashboard.jsx` → `/dashboard`, `_authed/index.jsx` → `/`; the segment adds nothing to the URL): +```javascript +// src/routes/_authed.jsx +import { createFileRoute } from "@tanstack/react-router"; +import { AuthGateError, requireSignedIn } from "@/components/auth/AuthGate"; +export const Route = createFileRoute("/_authed")({ ssr: false, beforeLoad: requireSignedIn, errorComponent: AuthGateError }); +``` +The gate resolves the session before any child loader runs, sends a signed-out visitor to `/login?returnTo=…` and shows the not-registered screen to a Base44 user who isn't a user of this app; a child reads the visitor with `Route.useRouteContext().user`. Never re-implement the gate inside a page, and never put an auth route under `_authed/`. + +Sign-in entry point: a Sign in button calls `useAuth().navigateToLogin()`, which sends the visitor to `/login` and back to the current page; Sign out calls `useAuth().logout()`. + +Customize auth pages only on explicit request; edit the components in place; preserve every flow step (register → OTP stays multi-step in any layout — never shortcut OTP with loginViaEmailPassword) and the `returnTo` handling: every page resolves a same-origin `returnTo` with `safeReturnTo()` from `src/lib/authReturnTo.js` (falling back to `'/'`) — keep it when editing, never hardcode a bare `'/'`. + +SDK (for template edits): +- loginViaEmailPassword(email, password) — sets token; then hard redirect to the resolved `returnTo` +- loginWithProvider(provider, fromUrl) — "google"|"facebook"|"microsoft"|"apple"; pass the resolved `returnTo` as fromUrl; redirects +- register({email, password}) — does not log in; user unverified. Required: register → OTP → verifyOtp → setToken(access_token) → hard redirect. Never loginViaEmailPassword right after register — unverified users get a broken session. +- verifyOtp({email, otpCode}) — returns access_token → setToken · resendOtp(email) +- resetPasswordRequest(email) — always show generic success (API hides whether email exists) · resetPassword({resetToken, newPassword}) — then hard-redirect to `/login` +- isAuthenticated() → Promise<boolean> · logout(redirectUrl?) +Hard redirects (window.location.href), never navigate() — the app must re-initialize with the new session. Auth calls throw: keep the pages' try/catch + inline errors + disabled submit while in-flight (exception: resetPasswordRequest always shows generic success). + +Missing component — only after reading the directory confirms it is actually absent (a build error alone never justifies recreation; check first): recreate it at the exact path/name per the rules above, reusing `AuthLayout` and `parts.jsx` in `src/components/auth/` — never "fix" by deleting a route or substituting something simpler. + +</authentication> + +<github_integration> +Two GitHub features exist: 2-way repo sync, and a GitHub connector (build features on GitHub data). On "GitHub", ask which one before proceeding. +</github_integration> + +<invite_user> +`await base44.users.inviteUser("foo@gmail.com", "user")` invites a user — email + app role "user" or "admin"; confirm which role the builder wants. + +Public app: admins invite both roles; regular users only role="user". Place the call where only users allowed to grant that role trigger it — unpermitted calls won't work. + +</invite_user> diff --git a/skills/base44-platform-workflows/SKILL.md b/skills/base44-platform-workflows/SKILL.md new file mode 100644 index 0000000..6e3851e --- /dev/null +++ b/skills/base44-platform-workflows/SKILL.md @@ -0,0 +1,26 @@ +--- +name: base44-platform-workflows +description: "Base44 workflows: trigger-driven automated processes defined as CNCF Serverless Workflow files under base44/workflows/ (scheduled, entity, connector, in-app agent, auth, publish and payment triggers; call, wait and switch steps; jq expressions), with the full authoring guide, plus the legacy automations pipeline for apps that predate workflows. Use for any 'when X happens, do Y' or 'every N hours, do Y'." +--- +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 workflows + +Workflows is the default pipeline: a `.jsonc` envelope under `base44/workflows/` is live once written. An app whose profile says `automation_pipeline: legacy_automations` uses the automations reference instead. + +> **Requires the Builder plan or above.** The workspace's entitlement is reported by the agent-profile endpoint; on a lower plan the platform rejects the deploy. + +## Pick the reference for this app + +Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (over the Base44 MCP `execute_api` +tool, or with your Base44 CLI token); it returns the framework, app type, runtime, +entitlements and the exact reference files to read. Without it, detect from the project files: +`src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a +native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. + +| Profile | Read | +|---|---| +| Web app, Vite SPA (the default web template) | [Workflows (Vite SPA: steps call backend functions)](references/web-spa.md), [Legacy automations (apps that predate workflows)](references/automations-legacy.md) | +| Web app, TanStack Start | [Workflows (TanStack Start: steps call server routes)](references/web-tanstack.md) | +| Native mobile app (Expo, React Native) | [Automations (mobile apps)](references/mobile-automations.md) | diff --git a/skills/base44-platform-workflows/references/automations-legacy.md b/skills/base44-platform-workflows/references/automations-legacy.md new file mode 100644 index 0000000..e875357 --- /dev/null +++ b/skills/base44-platform-workflows/references/automations-legacy.md @@ -0,0 +1,60 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 workflows: Legacy automations (apps that predate workflows) + +<automations_capability> +Automations run backend functions automatically — on a schedule, entity record changes, connector webhook events, or app users starting in-app agent conversations. + +<user_timezone> +Times the user mentions ("9am", "midnight") are local to them — ask for their timezone when it is unknown: +- one_time_date, ends_on_date, start_time take local time (auto-converted to UTC). +- cron_expression runs in UTC — compute the UTC equivalent yourself. +</user_timezone> + +<managing_automations> +This app runs the legacy automations pipeline (app profile `automation_pipeline: legacy_automations`). Automations are not files: create, update, toggle and delete them in the dashboard (Automations) or through the platform API (the `automations` area of the API catalog). The parameters below are the ones those surfaces accept, named as the builder passes them. +</managing_automations> + +<scheduled_automations> +automation_type="scheduled". Minimum interval: 5 minutes. +schedule_type="simple": repeat_interval=5, repeat_unit="minutes" | repeat_interval=1, repeat_unit="hours" | daily: repeat_interval=1, repeat_unit="days", start_time="09:00" | weekly: repeat_unit="weeks", repeat_on_days=[1, 5], start_time="10:00" (0=Sunday, 6=Saturday) | monthly: repeat_unit="months", repeat_on_day_of_month=15, start_time="00:00". +schedule_type="cron": standard "minute hour day-of-month month day-of-week" — "*/5 * * * *" = every 5 minutes, "0 9 * * 1-5" = 9am weekdays. +schedule_mode="one-time": one_time_date="2024-01-15T10:00:00". +Ends: ends_type="never" (default) | "on" + ends_on_date="2024-12-31T23:59:59" | "after" + ends_after_count=10. + +</scheduled_automations> + +<entity_automations> +automation_type="entity" — fires on entity record changes. Required: entity_name, event_types (array of "create", "update", "delete"). +Function payload: event { type, entity_name, entity_id }; data: current entity data; old_data: previous data ("update" only); changed_fields: top-level fields that changed (e.g. ["status", "amount"]); payload_too_large: true if entity data exceeded 200KB — data/old_data then null, fetch with `await base44.entities.get("entity_name", entity_id)`. + +Optional trigger_conditions pre-filter entity events. Conditions evaluate against a wrapped object (entity automations only, not connector): {"data": {...current...}, "old_data": {...previous, update only...}, "changed_fields": [...]}. Field paths take the wrapping prefix: "data.<field>", "old_data.<field>", or "changed_fields" with operator "contains" to test whether a field changed. + +</entity_automations> + +<in_app_agent_automations> +automation_type="in_app_agent" — fires when an app user starts a new in-app agent conversation. Use it to react to conversation starts / first messages — never a scheduled poll. +Required: function_name. Optional: agent_name (exact configured name; omit on create = all agents; "" on update clears), event_types (currently only "conversation_started"). +Function payload: automation { id, type, name }; event { type, agent_name, conversation_id, message_id, channel }; message { text, has_files }; app_user { id }; payload_too_large (message text omitted as too large). + +</in_app_agent_automations> + +<connector_automations> +automation_type="connector" — fires on a connected service's webhook events (new calendar event, Drive file change, …). +Required: integration_type (e.g. "googlecalendar", "googledrive"), events (array of event names), function_name. Optional: resource_id (Google Drive/Calendar — specific file/calendar to watch). +Prerequisites: connector authorized (see the `base44-platform-connectors` skill); handler function created first. Provider payloads/auth differ — read the connector's usage guide (`list_connectors` with the integration type) before writing the function. + +Optional trigger_conditions pre-filter webhook events: {"logic": "and", "conditions": [{"field": "<dot.path>", "operator": "<op>", "value": "<target>"}]} +- field: dot-path into the provider's raw webhook payload — no "data." prefix (unlike entity conditions); Slack: "event.channel", "event.text", "event.user". Take paths from the provider's public webhook payload docs. +- operator: equals | not_equals | gt | gte | lt | lte | contains | not_contains | starts_with | ends_with | in_list | not_in_list | exists | not_exists | is_empty | is_not_empty +- value: concrete value. If the user gives a human-readable name but the payload carries an internal ID, resolve it first: temporary backend function using the authorized connector, invoke it, use the returned ID — never guess IDs. E.g. #general Slack: conversations.list → {"field": "event.channel", "operator": "equals", "value": "C0123ABC"}. +</connector_automations> + +<usage_guidelines> +- To react to external events, prefer a connector automation (real-time webhooks) over a scheduled poll; check webhook support in the connector's details, poll only if no webhook-capable connector exists. +- Create the backend function before the automation; authorize connectors first. +- Check existing automations (dashboard or API) before adding one; descriptive names. +- Pause/resume from the dashboard or API. +</usage_guidelines> +</automations_capability> diff --git a/skills/base44-platform-workflows/references/mobile-automations.md b/skills/base44-platform-workflows/references/mobile-automations.md new file mode 100644 index 0000000..a862d39 --- /dev/null +++ b/skills/base44-platform-workflows/references/mobile-automations.md @@ -0,0 +1,157 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 workflows: Automations (mobile apps) + +<automations_capability> +You can manage automations to run backend functions automatically - on a schedule, when entity records change, when external services send webhook events, or when app users start in-app agent conversations. + +<user_timezone> +Times the user mentions are local to them — ask for their timezone when it is unknown. + +IMPORTANT: When the user mentions times (e.g., "9am", "2pm", "midnight"), they mean times in THEIR timezone. +- All times you provide (one_time_date, ends_on_date, start_time) should be in the user's local time - they will be automatically converted to UTC. +- For cron_expression: Times in cron are in UTC, so you need to calculate the UTC equivalent based on the user's timezone. +</user_timezone> + +<managing_automations> +Automations are not files: create, update, toggle and delete them in the dashboard (Automations) or through the platform API (the `automations` area of the API catalog). The parameters below are the ones those surfaces accept, named as the builder passes them. +</managing_automations> + +<automation_types> +**Scheduled Automations (automation_type="scheduled"):** +Run a function on a schedule. + +Simple schedules (schedule_type="simple"): +- Every 5 minutes: repeat_interval=5, repeat_unit="minutes" (minimum is 5 minutes) +- Hourly: repeat_interval=1, repeat_unit="hours" +- Daily at specific time: repeat_interval=1, repeat_unit="days", start_time="09:00" +- Weekly on specific days: repeat_unit="weeks", repeat_on_days=[1, 5], start_time="10:00" (0=Sunday, 6=Saturday) +- Monthly on specific day: repeat_unit="months", repeat_on_day_of_month=15, start_time="00:00" + +Cron schedules (schedule_type="cron"): +- Use standard cron: "minute hour day-of-month month day-of-week" +- Example: "*/5 * * * *" = every 5 minutes (minimum interval is 5 minutes) +- Example: "0 9 * * 1-5" = 9am every weekday + +One-time schedules (schedule_mode="one-time"): +- Run once: one_time_date="2024-01-15T10:00:00" + +End conditions: +- ends_type="never": Run indefinitely (default) +- ends_type="on": Run until a date (ends_on_date="2024-12-31T23:59:59") +- ends_type="after": Run N times (ends_after_count=10) + +**Entity Automations (automation_type="entity"):** +Run a function when entity records are created, updated, or deleted. + +Required: entity_name, event_types (array of: "create", "update", "delete") + +Examples: +- Send email on new order: entity_name="orders", event_types=["create"] +- Track status changes: entity_name="orders", event_types=["update"] +- Cleanup on delete: entity_name="users", event_types=["delete"] +- Multiple events: entity_name="orders", event_types=["create", "update"] + +The function receives a payload with: +- event: { type, entity_name, entity_id } +- data: current entity data (null if payload_too_large is true) +- old_data: previous data (only for "update" events, null if payload_too_large is true) +- changed_fields: array of top-level field names that changed between old_data and data (e.g. ["status", "amount"]) +- payload_too_large: true if entity data exceeded 200KB and was omitted. Use Base44 SDK to fetch: `await base44.entities.get("entity_name", entity_id)` + +Optional: trigger_conditions — pre-filter which entity events actually trigger execution. +Use when the user only wants to act on specific changes (e.g., only when status becomes "completed", or only when a specific field changed). + +Entity conditions evaluate against a wrapped object (this wrapping applies ONLY to entity automations, NOT connector automations): +{ + "data": { ...current entity fields... }, + "old_data": { ...previous entity fields (update events only)... }, + "changed_fields": ["field1", "field2", ...] +} + +Field paths MUST use the wrapping key as prefix: +- "data.<field>" — current entity value (e.g. "data.status") +- "old_data.<field>" — previous value before the update (e.g. "old_data.status") +- "changed_fields" — the list itself; use "contains" operator to check if a field changed + +Example — only fire when status changes to "completed": +trigger_conditions={ + "logic": "and", + "conditions": [ + {"field": "changed_fields", "operator": "contains", "value": "status"}, + {"field": "data.status", "operator": "equals", "value": "completed"} + ] +} + +**In-App Agent Automations (automation_type="in_app_agent"):** +Run a function when an app user starts a new conversation with an in-app agent. + +Required: function_name +Optional: agent_name (exact configured agent name; omit on create for all in-app agents; pass "" on update to clear a specific agent), event_types (array; currently only "conversation_started") + +Use this trigger when the user wants to react to app-user conversations in an in-app agent. Do NOT implement this with scheduled polling. + +The function receives a payload with: +- automation: { id, type, name } +- event: { type, agent_name, conversation_id, message_id, channel } +- message: { text, has_files } +- app_user: { id } +- payload_too_large: true if the message text was omitted because it was too large + +Example: +- Notify on a new support chat: agent_name="Support", event_types=["conversation_started"] + +**Connector Automations (automation_type="connector"):** +Run a function when a connected external service sends a webhook event (e.g., new calendar event, Google Drive file change). + +Required: integration_type (e.g., "googlecalendar", "googledrive"), events (array of event names), function_name +Optional: resource_id (for Google Drive/Calendar - specific file or calendar to watch) + +Prerequisites: +- The connector must be authorized first (see the `base44-platform-connectors` skill) +- The backend function to handle the webhook must exist before creating the automation + +Each provider has a different payload structure and auth model. Always read the connector's usage guide (`list_connectors` with the integration type) BEFORE writing the function. + +Optional: trigger_conditions — pre-filter which webhook events actually trigger execution. +Use when the user only wants to act on a subset of events (e.g., a specific Slack channel, a specific Drive folder). + +trigger_conditions format: +{ + "logic": "and", + "conditions": [ + {"field": "<dot.path>", "operator": "<op>", "value": "<target>"} + ] +} + +- "field": dot-path into the provider's RAW webhook payload. Do NOT prefix with "data." — unlike entity conditions, connector conditions evaluate directly against the webhook payload as received from the provider. + For example, for Slack: "event.channel", "event.text", "event.user" (NOT "data.event.channel"). + You MUST know the provider's webhook payload structure — consult the provider's public API docs to determine the correct field paths. +- "operator": equals | not_equals | gt | gte | lt | lte | contains | not_contains | starts_with | ends_with | in_list | not_in_list | exists | not_exists | is_empty | is_not_empty +- "value": the concrete value to match. When the user provides a human-readable name but the webhook payload carries an internal ID, resolve the name first: create a temporary backend function that uses the authorized connector to look up the ID, invoke it, then use the returned ID as the condition value. Never guess IDs. +</automation_types> + +<usage_guidelines> +Webhook vs polling: +When the user wants their app to react to external events (calendar changes, new messages, CRM updates, etc.), +Try using a connector automation (automation_type="connector") instead of a scheduled poll. +Connector automations deliver events in real time via webhooks and are more efficient and responsive than polling on a schedule. +Only fall back to scheduled polling when there is no connector with webhook support for the service in question. +Check the connector's details to see whether it supports webhooks before choosing scheduled polling. + +In-app agent conversation triggers: +When the user wants their app to react to a user starting or sending the first message in an in-app agent conversation, +use an in-app agent automation (automation_type="in_app_agent") with event_types=["conversation_started"]. +Do not create a scheduled automation that checks for recent conversations. + +Other guidelines: +1. First, ensure the backend function exists before creating an automation for it +2. For connector automations, ensure the connector is authorized before creating the automation +3. Check existing automations in the dashboard or API before adding one +4. Use descriptive names for automations to make them easy to identify +5. Pause/resume automations from the dashboard or API +6. Minimum interval for scheduled automations is 5 minutes +</usage_guidelines> + +</automations_capability> diff --git a/skills/base44-platform-workflows/references/web-spa.md b/skills/base44-platform-workflows/references/web-spa.md new file mode 100644 index 0000000..b27543b --- /dev/null +++ b/skills/base44-platform-workflows/references/web-spa.md @@ -0,0 +1,543 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 workflows: Workflows (Vite SPA: steps call backend functions) + +<workflows_capability> +Workflows: trigger-driven automated processes — one step (e.g. a scheduled function call) or complex (multi-step, `switch` branching, durable waits across hours/days, data threading, chained backend functions). For any "when X happens, do Y" / "every N hours/days, do Y", however many actions Y involves. + +<files_and_runs> +Workflow files live at `base44/workflows/<Name>.jsonc`; writing the file creates or updates the workflow, deleting it removes the workflow. Status and runs: `base44 workflows list --app-id <APP_ID>` and `base44 workflows runs --app-id <APP_ID> [--status failed]`, or the Workflows dashboard. +</files_and_runs> + +<workflow_building_blocks> +What workflows can be built from; the authoring guide that follows documents each in full. +## Workflow Building Blocks + +### Triggers (what starts a workflow) +- scheduled — Fires on a schedule — a cron cadence, a fixed interval, or a one-time future instant. +- entity — Fires when an entity record is created, updated, or deleted. +- connector — Fires on a real-time webhook event from an authorized connector (Slack, Google Calendar, Drive, …). +- in_app_agent — Fires when a conversation starts with an in-app agent. +- app_user_auth — Fires when an app user signs up or logs in. +- app_publish — Fires when the app is published from the builder. +- app_payment — Fires when a payment succeeds or is refunded in the app's payment provider. + +### Activities (what a `call` step can do) +- compute_seconds_until — Computes the seconds from now until a target datetime — feed the result into a dynamic wait. +- invoke_backend_function — Runs a deployed backend function — your code for API calls, data transforms, and entity CRUD. + +### Task Types (how steps are structured) +- call — invoke a registered activity with `with` args, then transition via `then`. +- wait — pause for an ISO-8601 duration (durable, survives restarts), then continue. +- switch — branch on ordered jq conditions; the last case is the catch-all `${ true }`. +</workflow_building_blocks> + +<creating_workflows> +1. Read the authoring guide below. 2. Needed backend functions first. 3. Connector triggers: authorize first. 4. Write `base44/workflows/<Name>.jsonc` (the envelope). 5. Auto-validation on save — fix anything reported as WORKFLOW ERROR. +Each write creates a new version; running workflows keep their starting version. Graph + run history: dashboard. +</creating_workflows> + +<debugging_workflows> +`base44 workflows runs --app-id <APP_ID> --status failed` lists failed runs with the failing task and its error; `base44 logs --function <name>` reads that function's own logs; the Workflows dashboard shows each run's steps. +</debugging_workflows> +</workflows_capability> + +# Workflow Authoring Guide + +## File Convention + +Workflows are `.jsonc` files under `base44/workflows/`. +- Path: `base44/workflows/<WorkflowName>.jsonc` +- Existing workflows: `base44 workflows list --app-id <APP_ID>` (runs: `base44 workflows runs --app-id <APP_ID>`), or the Workflows dashboard +- Create or update a workflow by writing its file +- Each write creates a new version. Already-running workflows keep the version they started with. +- After a successful write, the workflow is immediately active — scheduled triggers start firing, entity/connector triggers start listening. +- The workflow appears in the code view sidebar under Workflows and in the Workflows dashboard. + +## Managing Workflow Status + +Change lifecycle status (activate / deactivate / archive / unarchive) in the Workflows dashboard or through the platform API (the `workflows` area of the API catalog) — NOT file edits. + +- Stop/pause/cancel all: refresh the list, deactivate every active workflow, and report failures before claiming success. +- Destructive broad/bulk workflows: encode user protections in the definition (not conversation memory), preview scope/count, and get approval before activation or active updates. + +## Envelope Format + +```json +{ + "name": "Human-Readable Workflow Name", + "description": "What this workflow does", + "trigger": { ... }, + "definition": { ... } +} +``` + +## CNCF Serverless Workflow v1.0 Definition + +```json +{ + "document": { "dsl": "1.0.0", "name": "snake_case_id", "version": "1.0", "namespace": "base44" }, + "do": [ + { "task_name": { "call": "activity_name", "with": {}, "then": "next_task" } } + ] +} +``` + +**Task naming**: must be snake_case (letters, digits, underscores). Task results are accessed in jq as `${ .task_name.field }` — hyphens break jq field access. Never start a task name with `__` (double underscore) — that namespace is reserved for system steps and the save will be rejected. + +**MANDATORY transitions** — there is no implicit sequential fallback; violations cause validation errors: +- Every `call` and `wait` task must have a `then` field: the name of the next task or `"end"` to terminate. +- `switch` tasks branch ONLY through their cases: every case must have its own `then`, the **last case must be the catch-all `{"when": "${ true }", "then": ...}`** ("otherwise") so every run takes an explicit, visible branch, and the switch task itself must NOT have a task-level `then` — with an exhaustive case list it would be dead code. +- A switch needs at least one real condition case besides the catch-all. A switch whose only case is `${ true }` is a no-op and is rejected. + +**Execution model: strictly sequential.** Tasks run one at a time in `then` order — there is NO parallel execution, fork/join, or "branches running at the same time". Never fabricate "fan-out" / "converge" marker nodes (e.g. a pass-through switch) to make the graph look parallel: the graph would misrepresent what actually runs. If the user asks for parallel branches, explain that steps execute in sequence and model the flow as ordered steps. + +## Supported Task Types + +### call — invoke a registered activity +```json +{ "fetch_data": { "call": "invoke_backend_function", "with": { "function_name": "getData", "args": {} }, "then": "next_task" } } +``` + +### switch — conditional branching +```json +{ "check": { "switch": [ + { "when": "${ .fetch_data.status == \"active\" }", "then": "handle_active" }, + { "when": "${ true }", "then": "end" } +] } } +``` +Cases are evaluated in order — the first truthy `when` wins (transition rules above). + +### wait — durable time delay (survives restarts) +```json +{ "pause": { "wait": "PT5M", "then": "next_task" } } +``` +ISO 8601: PT5M = 5 min, PT1H = 1 hour, P1D = 1 day, P3DT12H = 3.5 days + +## `x-base44` (display-only node labels - set on every task) + +Set `x-base44` on every task for graph labels only — it never changes runtime behavior, task output, transitions, or activity inputs. Allowed sub-fields (anything else is rejected): `title` (node title — **always in the user's language**; the task key stays `snake_case` ASCII) and optional `description` (very short subtitle). + +```json +{ "fetch_data": { "call": "invoke_backend_function", "with": { "function_name": "getData" }, "then": "next_task", "x-base44": { "title": "Fetch order", "description": "Latest record" } } } +``` + +When editing, preserve `x-base44` where the task's purpose is unchanged; update `title`/`description` when the meaning changes — no stale labels. + +## Available Activities + +### compute_seconds_until +Compute seconds from now until a target datetime. +- `target_iso`: ISO 8601 datetime string +- Returns: {"seconds": float, "target_iso": str, "now_iso": str} + +### invoke_backend_function +Call a deployed Deno backend function. +- When to use: Logic you write as code: API calls, data fetching/transforms, entity CRUD, sending email/HTTP. You control exactly what it does — no agent reasoning turn. +- `function_name`: Name of the backend function to call +- `args`: Dict of arguments passed as the function request body (supports jq expressions) +- Returns: Parsed JSON response from the function + +## jq Expression Language + +Use `${ .field }` syntax (CNCF SWF v1.0). +- Trigger data: `${ .trigger.data.fieldname }` +- Task output: `${ .task_name.field }` +- Conditions: `${ .trigger.data.status == "active" }` +- String interpolation: `"Hello ${ .trigger.data.name }"` + +**Write jq, not JavaScript/Python.** Common mistakes to avoid: +- Logic operators are `and` / `or` / `not`, never `&&` / `||` / `!`: `${ .trigger.data.a == 1 and .trigger.data.b != 2 }`. +- Membership is `${ .trigger.data.event.channel | IN("C1","C2") }`, never infix `x in [...]`. +- String tests (`test` / `contains` / `startswith` / `ascii_downcase`) throw on a null field — default first: `${ (.trigger.data.subject // "") | test("receipt"; "i") }`. +- Iteration throws on null — default with `// []`: `${ any((.trigger.data.items // [])[]; .qty > 0) }`. +- In a pipe, `.` is the piped value, not the root — bind root refs first: `${ .trigger.data.event.channel as $c | ["C1","C2"] | index($c) != null }`. + +### Keep jq minimal + +jq only reads the payload (trigger data + prior task outputs): field access, equality, simple boolean/arithmetic checks. Anything more — transformation, computation, lookups, multi-step branching — belongs in a backend function (`invoke_backend_function`), not in elaborate jq. +- Never use `env` / `$ENV`, `input`, `def`, `range`, `recurse`, `reduce`, `foreach`, `walk`, `..` — the save is rejected. jq cannot reach app secrets; only a backend function can read and use an API key or signing secret. +- Good: `${ .trigger.data.status == "approved" }`. Bad: `${ env.API_KEY }`. + +## Trigger Types + +The trigger object wraps the type-specific fields (shown below) in `config`, plus an optional jq `condition` that must be truthy for the workflow to run: +```json +{ "config": { "trigger_type": "<type>", <type-specific fields — see your trigger's section> }, "condition": "${ <jq predicate over .trigger, optional> }" } +``` + +### scheduled +```json +{ "trigger_type": "scheduled", "cron_expression": "0 9 * * 1-5", "timezone": "America/New_York" } +``` + +**Webhook vs polling**: when the user wants to react to external events (calendar changes, new messages, CRM updates), prefer a connector trigger over scheduled polling — connector triggers deliver events in real time. Use scheduled polling only when no connector supports webhooks for the service (check the connector's details with `list_connectors`). + +**Timezone**: Ask the user for their timezone when it is not known. When the user mentions times (e.g., "9am", "2pm", "midnight"), they mean times in THEIR timezone. Set the `timezone` field to the user's timezone — the cron expression is then evaluated in that timezone (no manual UTC conversion needed). + +Cron format: `minute hour day-of-month month day-of-week` + +**Choose cron vs interval FIRST.** Cron fires at clock-aligned times, so a "every N <unit>" request is only expressible in cron when N evenly divides its field's wrap period — minutes where N divides 60 (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30) or hours where N divides 24 (1, 2, 3, 4, 6, 8, 12). For any other fixed gap — "every 90 minutes", "every 45 minutes", "every 7 hours", "every 2 days" — cron CANNOT express it: use **interval mode** (see below). Never approximate with `*/90 * * * *` (most cron parsers reduce the out-of-range minute step to :00, collapsing it to hourly — some reject it outright) or `0 9 */2 * *` (the day-of-month step restarts every month, so it is not a true 2-day gap). + +Translating user requests to cron (clock-aligned cadences): +- "every 5 minutes" → `*/5 * * * *` (minimum interval) +- "every hour" / "hourly" → `0 * * * *` +- "every day at 9am" / "daily at 9" → `0 9 * * *` +- "every weekday at 9am" / "Monday to Friday" → `0 9 * * 1-5` +- "every Monday at 10am" → `0 10 * * 1` +- "every Monday and Friday at 10am" → `0 10 * * 1,5` +- "every month on the 15th" → `0 0 15 * *` +- "every month on the 1st at 9am" → `0 9 1 * *` +- "twice a day at 9am and 5pm" → `0 9,17 * * *` +- "every 30 minutes during business hours" → `*/30 9-17 * * 1-5` +- "every Sunday at midnight" → `0 0 * * 0` + +**Run once** (instead of recurring): set `schedule_mode` to `"one_time"` with a future UTC instant — no cron_expression needed: +```json +{ "trigger_type": "scheduled", "schedule_mode": "one_time", "one_time_date": "2026-07-01T16:00:00Z" } +``` +`one_time_date` is a future UTC instant. Never guess "now" — take the current local time from the user, resolve the user's request against it (relative "in N minutes" or a clock time like "9:12am"), then convert to UTC. + +**Recurring at a fixed interval** — use this for any "every N minutes/hours/days" the cron rule above says cron can't express (e.g. "every 90 minutes", "every 45 minutes", "every 7 hours", "every 2 days"). Set `schedule_mode` to `"interval"` and DO NOT set `cron_expression`: +```json +{ "trigger_type": "scheduled", "schedule_mode": "interval", "interval_value": 2, "interval_unit": "days", "interval_anchor": "2026-07-01T09:00:00Z" } +``` +Units: minutes | hours | days only. Minimum interval is 5 minutes. `interval_anchor` (UTC) sets the phase — when the user gives a time-of-day or start ("every 2 days at 9am", "every 90 minutes starting 14:00") set it to that UTC instant; omit it to align to the epoch. For calendar cadences (specific weekdays, day-of-month, weekly, monthly) use cron. + +**End conditions** (recurring schedules only, default is run forever): +- Stop on a date: add `"ends_type": "on_date", "ends_on_date": "2026-12-31T23:59:00Z"` — the last instant the schedule may fire (UTC). +- Stop after N runs: add `"ends_type": "after_count", "ends_after_count": 10`. + +When a schedule finishes (one-time fired, end date passed, or run count reached) the workflow is automatically archived. + +### entity +```json +{ "trigger_type": "entity", "entity_name": "orders", "events": ["create", "update"] } +``` + +Trigger payload structure: +- `${ .trigger.entity_name }` — entity name (e.g. "orders") +- `${ .trigger.entity_id }` — the entity record ID +- `${ .trigger.event_type }` — "create", "update", or "delete" +- `${ .trigger.data.* }` — current entity data +- `${ .trigger.old_data.* }` — previous data (update events only) +- `${ .trigger.changed_fields }` — array of top-level field names that changed +- `${ .trigger.payload_too_large }` — true when the record was too large to attach (see note below) + +Condition examples: +- Only when status changes to "completed": + `"condition": "${ .trigger.data.status == \"completed\" and .trigger.old_data.status != \"completed\" }"` +- Only when a specific field changed: + `"condition": "${ .trigger.changed_fields | index(\"status\") != null }"` +- Only for a specific entity value: + `"condition": "${ .trigger.data.priority == \"high\" }"` + +Note: when the entity record is very large (over ~256KB), `data` and `old_data` arrive as null and `payload_too_large` is true (`changed_fields` stays populated). Trigger conditions still evaluate against the full data, but the backend function must re-fetch the record by id using the Base44 SDK: `await base44.entities.get("entity_name", entity_id)`. + +Use `read_file("base44/entities/<EntityName>.jsonc")` to check entity field names before writing conditions. + +### connector +```json +{"trigger_type": "connector", "integration_type": "slack", "events": ["message"]} +``` + +Optional: `resource_id` — for Google Calendar (calendar ID, e.g. "primary" or "user@gmail.com") or Google Drive (file ID). Controls which webhook registration is used (scopes the provider subscription, not per-workflow dispatch filtering). +```json +{"trigger_type": "connector", "integration_type": "googlecalendar", "events": ["events"], "resource_id": "primary"} +``` + +Prerequisites: +- The connector must be authorized (use the Base44 MCP connector tools `list_connectors` and `initiate_connector_connection`, or `base44 connectors`). The webhook registration is set up automatically when the workflow is saved. +- Each provider has a different payload structure and auth model. Always read the connector's usage guide (`list_connectors` with the integration type) for the provider-specific webhook handler guide BEFORE writing the backend function. + +Trigger payload: +- `${ .trigger.integration_type }` — e.g. "slack", "googlecalendar" +- `${ .trigger.event_type }` — the webhook event type +- `${ .trigger.data.* }` — the raw webhook payload from the provider +- `${ .trigger.payload_too_large }` — true when the webhook payload was too large to attach (over ~256KB); `data` arrives as null (conditions still evaluate against the full payload) and the backend function must re-fetch the resource from the provider + +Condition field paths use the provider's webhook payload structure: +- Slack: `${ .trigger.data.event.channel == \"C0123ABC\" }`, `${ .trigger.data.event.user == \"U0123ABC\" }` +- Google Calendar: `${ .trigger.data.resource_id == \"calendar_id\" }` + +**Important**: When the user provides a human-readable name (e.g. "#general"), the webhook payload uses internal IDs. Resolve the name to an ID first: create a temporary backend function that uses the authorized connector to look up the ID, invoke it once (`base44.functions.invoke`), then use the returned ID in the condition. Never guess IDs. + +High-volume connectors (e.g. Slack) require conditions — validation will reject triggers without them. + +If activation fails with `MISSING_WEBHOOK_SCOPES`, re-connect that connector (`initiate_connector_connection`, or `base44 connectors initiate`) with the union of its granted and missing scopes (re-authorization replaces the grant, so omitting the granted ones drops them), then save the workflow again. + +Available connectors: + (no connectors with webhook support) + +### in_app_agent +```json +{ "trigger_type": "in_app_agent", "agent_name": "support_agent", "events": ["conversation_started"] } +``` +Omit `agent_name` to trigger for all in-app agents. `events` is required — currently only `"conversation_started"` is supported. + +Trigger payload structure: +- `${ .trigger.agent_name }` — the agent name (top-level shorthand) +- `${ .trigger.data.event.type }` — always "conversation_started" +- `${ .trigger.data.event.agent_name }` — agent that received the message +- `${ .trigger.data.event.conversation_id }` — conversation ID +- `${ .trigger.data.event.message_id }` — first message ID +- `${ .trigger.data.event.channel }` — channel (e.g. "web", "mobile") +- `${ .trigger.data.message.text }` — first message text +- `${ .trigger.data.message.has_files }` — whether files were attached +- `${ .trigger.data.app_user.id }` — app user ID +- `${ .trigger.payload_too_large }` — true when the first message was too large to attach (over ~256KB); `data.message` arrives as null but the event IDs remain, so fetch the conversation by `data.event.conversation_id` + +### app_user_auth +```json +{ "trigger_type": "app_user_auth", "events": ["signup", "login"] } +``` + +app_user_auth fields live on `.trigger` directly — do NOT use `.trigger.data.*` for this trigger. + +Trigger payload fields (use `${ .trigger.<field> }` in conditions and `do` args): +- `${ .trigger.event_type }` — "signup" or "login" +- `${ .trigger.auth_method }` — "password", "google", "microsoft", "facebook", "apple", "app_sso", or "workspace_sso" +- `${ .trigger.occurred_at }` — ISO timestamp for the successful auth transaction +- `${ .trigger.user_id }` — app-user ID string (preferred for backend-function args) +- `${ .trigger.email }` — app-user email +- `${ .trigger.full_name }` — app-user display name +- `${ .trigger.role }` — app-user role +- `${ .trigger.app_user.id }` — same ID as user_id +- `${ .trigger.app_user.collaborator_role }` — collaborator role, when present +- `${ .trigger.app_user.is_verified }` — whether the app user is verified + +Canonical `invoke_backend_function` args (same for signup and login): +```json +"args": { + "user_id": "${ .trigger.user_id }", + "email": "${ .trigger.email }", + "auth_method": "${ .trigger.auth_method }" +} +``` +Do NOT pass `${ .trigger.app_user }` as user_id — it is an object, not a string. + +When each event fires (depends on whether the app is public or private): + +**Public apps:** +- **signup** — the end user's first successful sign-in (email/password or social login). +- **login** — every later successful sign-in. + +**Private or workspace apps:** +- **signup** — when an admin approves a new user's access request. Does not fire on first login. +- **login** — every successful sign-in after the user exists, including the first sign-in after approval. + +Condition examples: +- Only password auth: + `"condition": "${ .trigger.auth_method == \"password\" }"` +- Only Google signups: + `"condition": "${ .trigger.event_type == \"signup\" and .trigger.auth_method == \"google\" }"` +- Only workspace SSO logins: + `"condition": "${ .trigger.auth_method == \"workspace_sso\" }"` +- Only admin app users: + `"condition": "${ .trigger.role == \"admin\" }"` + +### app_publish +```json +{ "trigger_type": "app_publish", "events": ["published"] } +``` + +app_publish fields live on `.trigger` directly — do NOT use `.trigger.data.*` for this trigger. + +Trigger payload fields (use `${ .trigger.<field> }` in conditions and `do` args): +- `${ .trigger.event_type }` — always "published" +- `${ .trigger.occurred_at }` — ISO timestamp of the publish +- `${ .trigger.is_first_publish }` — true only on the app's first-ever publish +- `${ .trigger.visibility }` — published visibility ("private_with_login", "workspace_with_login", "public_with_login", or "public_without_login") +- `${ .trigger.published_by }` — platform user ID of the publisher, null for API-key publishes + +Fires on every successful publish, including restoring a previous version. Does not fire on unpublish. + +Condition examples: +- Only the first publish (launch): + `"condition": "${ .trigger.is_first_publish }"` + +### app_payment +```json +{ "trigger_type": "app_payment", "events": ["payment_succeeded"] } +``` + +Requires a connected payment provider (Stripe, Wix Payments, or Payments by Wix) — activating a workflow with this trigger fails when none is connected. Connect a provider first, then create the workflow. The payload is normalized across providers; workflows never receive provider-shaped raw data. + +Valid events: `payment_succeeded` (a successful payment — a completed checkout and the first payment of a subscription), `payment_refunded` (an approved refund). Failed payment attempts do NOT fire this trigger. + +Recurring subscription renewal charges are wired to fire `payment_succeeded` too, but delivery is not enabled yet for existing Stripe accounts (it needs a Stripe app permission + per-account re-consent). Do not build a workflow whose only purpose is reacting to renewals, and tell the user renewals may not fire yet if they ask for one. + +When renewals do arrive, the payload does not distinguish them from a first subscription payment — both are `payment_succeeded` with `mode: "subscription"`. So do not use this trigger for one-time-only actions on a subscription (welcome emails, provisioning, granting access) without your own guard, e.g. an entity record keyed by `provider_customer_id` that the workflow checks and writes. + +app_payment fields live on `.trigger` directly — do NOT use `.trigger.data.*` for this trigger. + +Trigger payload fields (use `${ .trigger.<field> }` in conditions and `do` args): +- `${ .trigger.event_type }` — "payment_succeeded" or "payment_refunded" +- `${ .trigger.provider }` — "stripe", "wix_payments", or "payments_by_wix" +- `${ .trigger.amount }` — integer amount in cents for most currencies (e.g. 2500 = $25.00), matching the platform's payment analytics representation +- `${ .trigger.currency }` — lowercase ISO currency code (e.g. "usd") +- `${ .trigger.is_live }` — false for test/sandbox payments; test payments DO fire this trigger +- `${ .trigger.mode }` — "payment" (one-off checkout) or "subscription" (a subscription charge — first payment OR renewal, indistinguishable); null on refunds. Only Stripe distinguishes these — Wix payments always report "payment" +- `${ .trigger.occurred_at }` — ISO timestamp of the provider event +- `${ .trigger.provider_customer_id }` — provider-side customer/contact ID, or null +- `${ .trigger.provider_transaction_id }` — provider-side transaction/session/refund ID; use it to re-fetch full details (payer email, line items) from a backend function via the provider's API. On Stripe refunds this is the CHARGE ID (`ch_...`), not a refund ID — retrieve the charge, which is also where the payer details live +- `${ .trigger.provider_event_id }` — provider webhook event ID (webhook redeliveries are already deduped on it — don't build your own guard) + +The payload carries no payer email or other PII — re-fetch by `provider_transaction_id` from a backend function when needed. Zero-amount events (free checkouts, 100% discounts) do not fire. + +Condition examples: +- Only real (live-mode) payments: + `"condition": "${ .trigger.is_live }"` +- Only large payments (over $100): + `"condition": "${ .trigger.amount > 10000 }"` + +## Designing Steps: When to Split vs Combine + +**Split** into separate tasks when operations are independent (a failure shouldn't re-run the others), a delay sits between them, or you want per-step visibility and retry granularity in the run log. **Combine** into one step when operations share setup/auth/transaction, must be atomic, or the second needs the first's full in-memory result. + +**Conditions**: use a `switch` when the outcome picks the path (skip steps, branch, end early — visible in the graph); keep validation/error handling inside the step. Rule of thumb: a flowchart diamond goes in the workflow; an implementation if-statement stays in the step. + +## Important Notes + +- Create the backend function BEFORE the workflow that calls it. +- A step calls the backend function as the workflow's creator, an app admin, but its URL is public. One only workflows call must start with `const user = await base44.auth.me()`, return 401 when there is no user and 403 unless `user.role === 'admin'`, then use `base44.asServiceRole`; never a shared secret. One the app's users also call keeps its usual checks. +- Never put a secret value (API key, token, password) in a workflow — not in step args, conditions or anywhere else. The backend function reads the secret from the app's secrets itself. +- Infinite loop prevention: if a workflow has an entity trigger AND creates the same entity type, add a condition to distinguish source entities from workflow-created ones. +- Validation runs automatically on write. Fix errors shown in WORKFLOW ERROR messages and retry. +- Some workflow files carry an `x-base44-migrated-from-automation` marker naming a `legacy_payload_function`. The step calling that function is on a compatibility layer keyed to the function's **name** — while the step calls it, the function receives a legacy input shape. You can change the function's internal logic, but don't change how it consumes its input. To change its inputs or replace it, point the step at a **new** function (different name) written normally — the different name turns the layer off. Other and new steps are normal. + +## Examples + +> Envelope examples below use the builder's `write_file(path, envelope)` form: create the file at `path` with the envelope as its content. + +### "Clean up old data every night at 2am" +1. Create backend function `base44/functions/cleanupOldData/entry.ts` +2. Create workflow: +```json +write_file("base44/workflows/Nightly Cleanup.jsonc", { + "name": "Nightly Cleanup", + "description": "Cleans up old data every night at 2am", + "trigger": { "config": { "trigger_type": "scheduled", "cron_expression": "0 2 * * *", "timezone": "<user_timezone>" } }, + "definition": { + "document": { "dsl": "1.0.0", "name": "nightly_cleanup", "version": "1.0", "namespace": "base44" }, + "do": [{ "cleanup": { "call": "invoke_backend_function", "with": { "function_name": "cleanupOldData" }, "then": "end" } }] + } +}) +``` + +### "When an order status changes to completed, send a confirmation and wait 3 days, then request a review" +1. Create two backend functions: `sendConfirmation` and `requestReview` +2. Create workflow: +```json +write_file("base44/workflows/Order Followup.jsonc", { + "name": "Order Followup", + "description": "Sends confirmation on order completion, then requests review after 3 days", + "trigger": { + "config": { "trigger_type": "entity", "entity_name": "orders", "events": ["update"] }, + "condition": "${ .trigger.data.status == \"completed\" and .trigger.old_data.status != \"completed\" }" + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "order_followup", "version": "1.0", "namespace": "base44" }, + "do": [ + { "send_confirmation": { "call": "invoke_backend_function", "with": { "function_name": "sendConfirmation", "args": { "order_id": "${ .trigger.entity_id }" } }, "then": "wait_period" } }, + { "wait_period": { "wait": "P3D", "then": "request_review" } }, + { "request_review": { "call": "invoke_backend_function", "with": { "function_name": "requestReview", "args": { "order_id": "${ .trigger.entity_id }" } }, "then": "end" } } + ] + } +}) +``` + +### "When someone messages in #general on Slack, log it" +1. Ensure Slack is connected (`list_connectors`, `initiate_connector_connection`) +2. Resolve "#general" to channel ID: create temp function, call Slack API, get "C0123ABC" +3. Create backend function: `logSlackMessage` +4. Create workflow with condition: +```json +write_file("base44/workflows/Slack Logger.jsonc", { + "name": "Slack Logger", + "description": "Logs messages from #general channel", + "trigger": { + "config": { "trigger_type": "connector", "integration_type": "slack", "events": ["message"] }, + "condition": "${ .trigger.data.event.channel == \"C0123ABC\" }" + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "slack_logger", "version": "1.0", "namespace": "base44" }, + "do": [{ "log_message": { "call": "invoke_backend_function", "with": { "function_name": "logSlackMessage", "args": { "text": "${ .trigger.data.event.text }", "user": "${ .trigger.data.event.user }" } }, "then": "end" } }] + } +}) +``` + +### "When someone starts a chat with the Support agent, create a ticket and notify the team" +1. Create two backend functions: `createSupportTicket` and `notifyTeam` +2. Create workflow: +```json +write_file("base44/workflows/Support Ticket.jsonc", { + "name": "Support Ticket", + "description": "Creates a ticket and notifies team when a support conversation starts", + "trigger": { + "config": { "trigger_type": "in_app_agent", "agent_name": "Support", "events": ["conversation_started"] } + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "support_ticket", "version": "1.0", "namespace": "base44" }, + "do": [ + { "create_ticket": { "call": "invoke_backend_function", "with": { "function_name": "createSupportTicket", "args": { "message": "${ .trigger.data.message.text }", "user_id": "${ .trigger.data.app_user.id }" } }, "then": "notify" } }, + { "notify": { "call": "invoke_backend_function", "with": { "function_name": "notifyTeam", "args": { "ticket_id": "${ .create_ticket.id }", "agent": "${ .trigger.data.event.agent_name }" } }, "then": "end" } } + ] + } +}) +``` + +### "When someone signs up or logs in with a password, record it in the audit log" +1. Create backend function: `onPasswordAuth` +2. Create workflow: +```json +write_file("base44/workflows/On Password Auth.jsonc", { + "name": "On Password Auth", + "description": "Records password signups and logins in the audit log", + "trigger": { + "config": { "trigger_type": "app_user_auth", "events": ["login", "signup"] }, + "condition": "${ .trigger.auth_method == \"password\" }" + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "on_password_auth", "version": "1.0", "namespace": "base44" }, + "do": [{ "write_audit": { "call": "invoke_backend_function", "with": { "function_name": "onPasswordAuth", "args": { "user_id": "${ .trigger.user_id }", "email": "${ .trigger.email }", "activity": "${ .trigger.event_type }" } }, "then": "end" } }] + } +}) +``` + +### "When the app is published for the first time, notify the team" +1. Create backend function: `onAppLaunched` +2. Create workflow: +```json +write_file("base44/workflows/On First Publish.jsonc", { + "name": "On First Publish", + "description": "Notifies the team when the app is published for the first time", + "trigger": { + "config": { "trigger_type": "app_publish", "events": ["published"] }, + "condition": "${ .trigger.is_first_publish }" + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "on_first_publish", "version": "1.0", "namespace": "base44" }, + "do": [{ "notify_team": { "call": "invoke_backend_function", "with": { "function_name": "onAppLaunched", "args": { "published_at": "${ .trigger.occurred_at }", "visibility": "${ .trigger.visibility }" } }, "then": "end" } }] + } +}) +``` + +### "When someone pays, send a thank-you email" +1. Create backend function: `sendPaymentThankYou` — it looks up the payer's email from the provider API using the transaction ID, then sends the email +2. Create workflow: +```json +write_file("base44/workflows/On Payment.jsonc", { + "name": "On Payment", + "description": "Sends a thank-you email when a live payment succeeds", + "trigger": { + "config": { "trigger_type": "app_payment", "events": ["payment_succeeded"] }, + "condition": "${ .trigger.is_live }" + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "on_payment", "version": "1.0", "namespace": "base44" }, + "do": [{ "send_thank_you": { "call": "invoke_backend_function", "with": { "function_name": "sendPaymentThankYou", "args": { "transaction_id": "${ .trigger.provider_transaction_id }", "amount": "${ .trigger.amount }", "currency": "${ .trigger.currency }" } }, "then": "end" } }] + } +}) +``` diff --git a/skills/base44-platform-workflows/references/web-tanstack.md b/skills/base44-platform-workflows/references/web-tanstack.md new file mode 100644 index 0000000..a786aa9 --- /dev/null +++ b/skills/base44-platform-workflows/references/web-tanstack.md @@ -0,0 +1,545 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 workflows: Workflows (TanStack Start: steps call server routes) + +<workflows_capability> +Workflows: trigger-driven automated processes — one step (e.g. a scheduled function call) or complex (multi-step, `switch` branching, durable waits across hours/days, data threading, chained backend functions). For any "when X happens, do Y" / "every N hours/days, do Y", however many actions Y involves. + +<files_and_runs> +Workflow files live at `base44/workflows/<Name>.jsonc`; writing the file creates or updates the workflow, deleting it removes the workflow. Status and runs: `base44 workflows list --app-id <APP_ID>` and `base44 workflows runs --app-id <APP_ID> [--status failed]`, or the Workflows dashboard. +</files_and_runs> + +<workflow_building_blocks> +What workflows can be built from; the authoring guide that follows documents each in full. +## Workflow Building Blocks + +### Triggers (what starts a workflow) +- scheduled — Fires on a schedule — a cron cadence, a fixed interval, or a one-time future instant. +- entity — Fires when an entity record is created, updated, or deleted. +- connector — Fires on a real-time webhook event from an authorized connector (Slack, Google Calendar, Drive, …). +- in_app_agent — Fires when a conversation starts with an in-app agent. +- app_user_auth — Fires when an app user signs up or logs in. +- app_publish — Fires when the app is published from the builder. +- app_payment — Fires when a payment succeeds or is refunded in the app's payment provider. + +### Activities (what a `call` step can do) +- compute_seconds_until — Computes the seconds from now until a target datetime — feed the result into a dynamic wait. +- invoke_server_route — Calls a route on the app's own server. + +### Task Types (how steps are structured) +- call — invoke a registered activity with `with` args, then transition via `then`. +- wait — pause for an ISO-8601 duration (durable, survives restarts), then continue. +- switch — branch on ordered jq conditions; the last case is the catch-all `${ true }`. +</workflow_building_blocks> + +<creating_workflows> +1. Read the authoring guide below. 2. Needed server routes first. 3. Connector triggers: authorize first. 4. Write `base44/workflows/<Name>.jsonc` (the envelope). 5. Auto-validation on save — fix anything reported as WORKFLOW ERROR. +Each write creates a new version; running workflows keep their starting version. Graph + run history: dashboard. +</creating_workflows> + +<debugging_workflows> +`base44 workflows runs --app-id <APP_ID> --status failed` lists failed runs with the failing task and its error; `base44 logs --function <name>` reads that function's own logs; the Workflows dashboard shows each run's steps. +</debugging_workflows> +</workflows_capability> + +# Workflow Authoring Guide + +## File Convention + +Workflows are `.jsonc` files under `base44/workflows/`. +- Path: `base44/workflows/<WorkflowName>.jsonc` +- Existing workflows: `base44 workflows list --app-id <APP_ID>` (runs: `base44 workflows runs --app-id <APP_ID>`), or the Workflows dashboard +- Create or update a workflow by writing its file +- Each write creates a new version. Already-running workflows keep the version they started with. +- After a successful write, the workflow is immediately active — scheduled triggers start firing, entity/connector triggers start listening. +- The workflow appears in the code view sidebar under Workflows and in the Workflows dashboard. + +## Managing Workflow Status + +Change lifecycle status (activate / deactivate / archive / unarchive) in the Workflows dashboard or through the platform API (the `workflows` area of the API catalog) — NOT file edits. + +- Stop/pause/cancel all: refresh the list, deactivate every active workflow, and report failures before claiming success. +- Destructive broad/bulk workflows: encode user protections in the definition (not conversation memory), preview scope/count, and get approval before activation or active updates. + +## Envelope Format + +```json +{ + "name": "Human-Readable Workflow Name", + "description": "What this workflow does", + "trigger": { ... }, + "definition": { ... } +} +``` + +## CNCF Serverless Workflow v1.0 Definition + +```json +{ + "document": { "dsl": "1.0.0", "name": "snake_case_id", "version": "1.0", "namespace": "base44" }, + "do": [ + { "task_name": { "call": "activity_name", "with": {}, "then": "next_task" } } + ] +} +``` + +**Task naming**: must be snake_case (letters, digits, underscores). Task results are accessed in jq as `${ .task_name.field }` — hyphens break jq field access. Never start a task name with `__` (double underscore) — that namespace is reserved for system steps and the save will be rejected. + +**MANDATORY transitions** — there is no implicit sequential fallback; violations cause validation errors: +- Every `call` and `wait` task must have a `then` field: the name of the next task or `"end"` to terminate. +- `switch` tasks branch ONLY through their cases: every case must have its own `then`, the **last case must be the catch-all `{"when": "${ true }", "then": ...}`** ("otherwise") so every run takes an explicit, visible branch, and the switch task itself must NOT have a task-level `then` — with an exhaustive case list it would be dead code. +- A switch needs at least one real condition case besides the catch-all. A switch whose only case is `${ true }` is a no-op and is rejected. + +**Execution model: strictly sequential.** Tasks run one at a time in `then` order — there is NO parallel execution, fork/join, or "branches running at the same time". Never fabricate "fan-out" / "converge" marker nodes (e.g. a pass-through switch) to make the graph look parallel: the graph would misrepresent what actually runs. If the user asks for parallel branches, explain that steps execute in sequence and model the flow as ordered steps. + +## Supported Task Types + +### call — invoke a registered activity +```json +{ "fetch_data": { "call": "invoke_server_route", "with": { "path": "/api/get-data", "body": {} }, "then": "next_task" } } +``` + +### switch — conditional branching +```json +{ "check": { "switch": [ + { "when": "${ .fetch_data.status == \"active\" }", "then": "handle_active" }, + { "when": "${ true }", "then": "end" } +] } } +``` +Cases are evaluated in order — the first truthy `when` wins (transition rules above). + +### wait — durable time delay (survives restarts) +```json +{ "pause": { "wait": "PT5M", "then": "next_task" } } +``` +ISO 8601: PT5M = 5 min, PT1H = 1 hour, P1D = 1 day, P3DT12H = 3.5 days + +## `x-base44` (display-only node labels - set on every task) + +Set `x-base44` on every task for graph labels only — it never changes runtime behavior, task output, transitions, or activity inputs. Allowed sub-fields (anything else is rejected): `title` (node title — **always in the user's language**; the task key stays `snake_case` ASCII) and optional `description` (very short subtitle). + +```json +{ "fetch_data": { "call": "invoke_server_route", "with": { "path": "/api/get-data" }, "then": "next_task", "x-base44": { "title": "Fetch order", "description": "Latest record" } } } +``` + +When editing, preserve `x-base44` where the task's purpose is unchanged; update `title`/`description` when the meaning changes — no stale labels. + +## Available Activities + +### compute_seconds_until +Compute seconds from now until a target datetime. +- `target_iso`: ISO 8601 datetime string +- Returns: {"seconds": float, "target_iso": str, "now_iso": str} + +### invoke_server_route +Call a route served by the app's own full-stack server. +- When to use: Logic you write as code in one of the app's server routes: API calls, data fetching/transforms, entity CRUD, sending email/HTTP — no agent reasoning turn. A path that matches no route reaches the app's catch-all instead of failing, so copy it from the route file rather than guessing. +- `path`: App-relative route path, starting with '/' (e.g. '/api/orders/sync') +- `method`: HTTP method — GET, POST, PUT, PATCH or DELETE (default POST) +- `body`: Dict sent as the JSON request body (supports jq expressions) +- `query`: Dict of query-string parameters (supports jq expressions) +- Returns: Parsed JSON response from the route + +## jq Expression Language + +Use `${ .field }` syntax (CNCF SWF v1.0). +- Trigger data: `${ .trigger.data.fieldname }` +- Task output: `${ .task_name.field }` +- Conditions: `${ .trigger.data.status == "active" }` +- String interpolation: `"Hello ${ .trigger.data.name }"` + +**Write jq, not JavaScript/Python.** Common mistakes to avoid: +- Logic operators are `and` / `or` / `not`, never `&&` / `||` / `!`: `${ .trigger.data.a == 1 and .trigger.data.b != 2 }`. +- Membership is `${ .trigger.data.event.channel | IN("C1","C2") }`, never infix `x in [...]`. +- String tests (`test` / `contains` / `startswith` / `ascii_downcase`) throw on a null field — default first: `${ (.trigger.data.subject // "") | test("receipt"; "i") }`. +- Iteration throws on null — default with `// []`: `${ any((.trigger.data.items // [])[]; .qty > 0) }`. +- In a pipe, `.` is the piped value, not the root — bind root refs first: `${ .trigger.data.event.channel as $c | ["C1","C2"] | index($c) != null }`. + +### Keep jq minimal + +jq only reads the payload (trigger data + prior task outputs): field access, equality, simple boolean/arithmetic checks. Anything more — transformation, computation, lookups, multi-step branching — belongs in a server route (`invoke_server_route`), not in elaborate jq. +- Never use `env` / `$ENV`, `input`, `def`, `range`, `recurse`, `reduce`, `foreach`, `walk`, `..` — the save is rejected. jq cannot reach app secrets; only a server route can read and use an API key or signing secret. +- Good: `${ .trigger.data.status == "approved" }`. Bad: `${ env.API_KEY }`. + +## Trigger Types + +The trigger object wraps the type-specific fields (shown below) in `config`, plus an optional jq `condition` that must be truthy for the workflow to run: +```json +{ "config": { "trigger_type": "<type>", <type-specific fields — see your trigger's section> }, "condition": "${ <jq predicate over .trigger, optional> }" } +``` + +### scheduled +```json +{ "trigger_type": "scheduled", "cron_expression": "0 9 * * 1-5", "timezone": "America/New_York" } +``` + +**Webhook vs polling**: when the user wants to react to external events (calendar changes, new messages, CRM updates), prefer a connector trigger over scheduled polling — connector triggers deliver events in real time. Use scheduled polling only when no connector supports webhooks for the service (check the connector's details with `list_connectors`). + +**Timezone**: Ask the user for their timezone when it is not known. When the user mentions times (e.g., "9am", "2pm", "midnight"), they mean times in THEIR timezone. Set the `timezone` field to the user's timezone — the cron expression is then evaluated in that timezone (no manual UTC conversion needed). + +Cron format: `minute hour day-of-month month day-of-week` + +**Choose cron vs interval FIRST.** Cron fires at clock-aligned times, so a "every N <unit>" request is only expressible in cron when N evenly divides its field's wrap period — minutes where N divides 60 (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30) or hours where N divides 24 (1, 2, 3, 4, 6, 8, 12). For any other fixed gap — "every 90 minutes", "every 45 minutes", "every 7 hours", "every 2 days" — cron CANNOT express it: use **interval mode** (see below). Never approximate with `*/90 * * * *` (most cron parsers reduce the out-of-range minute step to :00, collapsing it to hourly — some reject it outright) or `0 9 */2 * *` (the day-of-month step restarts every month, so it is not a true 2-day gap). + +Translating user requests to cron (clock-aligned cadences): +- "every 5 minutes" → `*/5 * * * *` (minimum interval) +- "every hour" / "hourly" → `0 * * * *` +- "every day at 9am" / "daily at 9" → `0 9 * * *` +- "every weekday at 9am" / "Monday to Friday" → `0 9 * * 1-5` +- "every Monday at 10am" → `0 10 * * 1` +- "every Monday and Friday at 10am" → `0 10 * * 1,5` +- "every month on the 15th" → `0 0 15 * *` +- "every month on the 1st at 9am" → `0 9 1 * *` +- "twice a day at 9am and 5pm" → `0 9,17 * * *` +- "every 30 minutes during business hours" → `*/30 9-17 * * 1-5` +- "every Sunday at midnight" → `0 0 * * 0` + +**Run once** (instead of recurring): set `schedule_mode` to `"one_time"` with a future UTC instant — no cron_expression needed: +```json +{ "trigger_type": "scheduled", "schedule_mode": "one_time", "one_time_date": "2026-07-01T16:00:00Z" } +``` +`one_time_date` is a future UTC instant. Never guess "now" — take the current local time from the user, resolve the user's request against it (relative "in N minutes" or a clock time like "9:12am"), then convert to UTC. + +**Recurring at a fixed interval** — use this for any "every N minutes/hours/days" the cron rule above says cron can't express (e.g. "every 90 minutes", "every 45 minutes", "every 7 hours", "every 2 days"). Set `schedule_mode` to `"interval"` and DO NOT set `cron_expression`: +```json +{ "trigger_type": "scheduled", "schedule_mode": "interval", "interval_value": 2, "interval_unit": "days", "interval_anchor": "2026-07-01T09:00:00Z" } +``` +Units: minutes | hours | days only. Minimum interval is 5 minutes. `interval_anchor` (UTC) sets the phase — when the user gives a time-of-day or start ("every 2 days at 9am", "every 90 minutes starting 14:00") set it to that UTC instant; omit it to align to the epoch. For calendar cadences (specific weekdays, day-of-month, weekly, monthly) use cron. + +**End conditions** (recurring schedules only, default is run forever): +- Stop on a date: add `"ends_type": "on_date", "ends_on_date": "2026-12-31T23:59:00Z"` — the last instant the schedule may fire (UTC). +- Stop after N runs: add `"ends_type": "after_count", "ends_after_count": 10`. + +When a schedule finishes (one-time fired, end date passed, or run count reached) the workflow is automatically archived. + +### entity +```json +{ "trigger_type": "entity", "entity_name": "orders", "events": ["create", "update"] } +``` + +Trigger payload structure: +- `${ .trigger.entity_name }` — entity name (e.g. "orders") +- `${ .trigger.entity_id }` — the entity record ID +- `${ .trigger.event_type }` — "create", "update", or "delete" +- `${ .trigger.data.* }` — current entity data +- `${ .trigger.old_data.* }` — previous data (update events only) +- `${ .trigger.changed_fields }` — array of top-level field names that changed +- `${ .trigger.payload_too_large }` — true when the record was too large to attach (see note below) + +Condition examples: +- Only when status changes to "completed": + `"condition": "${ .trigger.data.status == \"completed\" and .trigger.old_data.status != \"completed\" }"` +- Only when a specific field changed: + `"condition": "${ .trigger.changed_fields | index(\"status\") != null }"` +- Only for a specific entity value: + `"condition": "${ .trigger.data.priority == \"high\" }"` + +Note: when the entity record is very large (over ~256KB), `data` and `old_data` arrive as null and `payload_too_large` is true (`changed_fields` stays populated). Trigger conditions still evaluate against the full data, but the server route must re-fetch the record by id using the Base44 SDK: `await base44.entities.get("entity_name", entity_id)`. + +Use `read_file("base44/entities/<EntityName>.jsonc")` to check entity field names before writing conditions. + +### connector +```json +{"trigger_type": "connector", "integration_type": "slack", "events": ["message"]} +``` + +Optional: `resource_id` — for Google Calendar (calendar ID, e.g. "primary" or "user@gmail.com") or Google Drive (file ID). Controls which webhook registration is used (scopes the provider subscription, not per-workflow dispatch filtering). +```json +{"trigger_type": "connector", "integration_type": "googlecalendar", "events": ["events"], "resource_id": "primary"} +``` + +Prerequisites: +- The connector must be authorized (use the Base44 MCP connector tools `list_connectors` and `initiate_connector_connection`, or `base44 connectors`). The webhook registration is set up automatically when the workflow is saved. +- Each provider has a different payload structure and auth model. Always read the connector's usage guide (`list_connectors` with the integration type) for the provider-specific webhook handler guide BEFORE writing the server route. + +Trigger payload: +- `${ .trigger.integration_type }` — e.g. "slack", "googlecalendar" +- `${ .trigger.event_type }` — the webhook event type +- `${ .trigger.data.* }` — the raw webhook payload from the provider +- `${ .trigger.payload_too_large }` — true when the webhook payload was too large to attach (over ~256KB); `data` arrives as null (conditions still evaluate against the full payload) and the server route must re-fetch the resource from the provider + +Condition field paths use the provider's webhook payload structure: +- Slack: `${ .trigger.data.event.channel == \"C0123ABC\" }`, `${ .trigger.data.event.user == \"U0123ABC\" }` +- Google Calendar: `${ .trigger.data.resource_id == \"calendar_id\" }` + +**Important**: When the user provides a human-readable name (e.g. "#general"), the webhook payload uses internal IDs. Resolve the name to an ID first: create a temporary server route that uses the authorized connector to look up the ID, request it once, then use the returned ID in the condition. Never guess IDs. + +High-volume connectors (e.g. Slack) require conditions — validation will reject triggers without them. + +If activation fails with `MISSING_WEBHOOK_SCOPES`, re-connect that connector (`initiate_connector_connection`, or `base44 connectors initiate`) with the union of its granted and missing scopes (re-authorization replaces the grant, so omitting the granted ones drops them), then save the workflow again. + +Available connectors: + (no connectors with webhook support) + +### in_app_agent +```json +{ "trigger_type": "in_app_agent", "agent_name": "support_agent", "events": ["conversation_started"] } +``` +Omit `agent_name` to trigger for all in-app agents. `events` is required — currently only `"conversation_started"` is supported. + +Trigger payload structure: +- `${ .trigger.agent_name }` — the agent name (top-level shorthand) +- `${ .trigger.data.event.type }` — always "conversation_started" +- `${ .trigger.data.event.agent_name }` — agent that received the message +- `${ .trigger.data.event.conversation_id }` — conversation ID +- `${ .trigger.data.event.message_id }` — first message ID +- `${ .trigger.data.event.channel }` — channel (e.g. "web", "mobile") +- `${ .trigger.data.message.text }` — first message text +- `${ .trigger.data.message.has_files }` — whether files were attached +- `${ .trigger.data.app_user.id }` — app user ID +- `${ .trigger.payload_too_large }` — true when the first message was too large to attach (over ~256KB); `data.message` arrives as null but the event IDs remain, so fetch the conversation by `data.event.conversation_id` + +### app_user_auth +```json +{ "trigger_type": "app_user_auth", "events": ["signup", "login"] } +``` + +app_user_auth fields live on `.trigger` directly — do NOT use `.trigger.data.*` for this trigger. + +Trigger payload fields (use `${ .trigger.<field> }` in conditions and `do` args): +- `${ .trigger.event_type }` — "signup" or "login" +- `${ .trigger.auth_method }` — "password", "google", "microsoft", "facebook", "apple", "app_sso", or "workspace_sso" +- `${ .trigger.occurred_at }` — ISO timestamp for the successful auth transaction +- `${ .trigger.user_id }` — app-user ID string (preferred for server-route body) +- `${ .trigger.email }` — app-user email +- `${ .trigger.full_name }` — app-user display name +- `${ .trigger.role }` — app-user role +- `${ .trigger.app_user.id }` — same ID as user_id +- `${ .trigger.app_user.collaborator_role }` — collaborator role, when present +- `${ .trigger.app_user.is_verified }` — whether the app user is verified + +Canonical `invoke_server_route` body (same for signup and login): +```json +"body": { + "user_id": "${ .trigger.user_id }", + "email": "${ .trigger.email }", + "auth_method": "${ .trigger.auth_method }" +} +``` +Do NOT pass `${ .trigger.app_user }` as user_id — it is an object, not a string. + +When each event fires (depends on whether the app is public or private): + +**Public apps:** +- **signup** — the end user's first successful sign-in (email/password or social login). +- **login** — every later successful sign-in. + +**Private or workspace apps:** +- **signup** — when an admin approves a new user's access request. Does not fire on first login. +- **login** — every successful sign-in after the user exists, including the first sign-in after approval. + +Condition examples: +- Only password auth: + `"condition": "${ .trigger.auth_method == \"password\" }"` +- Only Google signups: + `"condition": "${ .trigger.event_type == \"signup\" and .trigger.auth_method == \"google\" }"` +- Only workspace SSO logins: + `"condition": "${ .trigger.auth_method == \"workspace_sso\" }"` +- Only admin app users: + `"condition": "${ .trigger.role == \"admin\" }"` + +### app_publish +```json +{ "trigger_type": "app_publish", "events": ["published"] } +``` + +app_publish fields live on `.trigger` directly — do NOT use `.trigger.data.*` for this trigger. + +Trigger payload fields (use `${ .trigger.<field> }` in conditions and `do` args): +- `${ .trigger.event_type }` — always "published" +- `${ .trigger.occurred_at }` — ISO timestamp of the publish +- `${ .trigger.is_first_publish }` — true only on the app's first-ever publish +- `${ .trigger.visibility }` — published visibility ("private_with_login", "workspace_with_login", "public_with_login", or "public_without_login") +- `${ .trigger.published_by }` — platform user ID of the publisher, null for API-key publishes + +Fires on every successful publish, including restoring a previous version. Does not fire on unpublish. + +Condition examples: +- Only the first publish (launch): + `"condition": "${ .trigger.is_first_publish }"` + +### app_payment +```json +{ "trigger_type": "app_payment", "events": ["payment_succeeded"] } +``` + +Requires a connected payment provider (Stripe, Wix Payments, or Payments by Wix) — activating a workflow with this trigger fails when none is connected. Connect a provider first, then create the workflow. The payload is normalized across providers; workflows never receive provider-shaped raw data. + +Valid events: `payment_succeeded` (a successful payment — a completed checkout and the first payment of a subscription), `payment_refunded` (an approved refund). Failed payment attempts do NOT fire this trigger. + +Recurring subscription renewal charges are wired to fire `payment_succeeded` too, but delivery is not enabled yet for existing Stripe accounts (it needs a Stripe app permission + per-account re-consent). Do not build a workflow whose only purpose is reacting to renewals, and tell the user renewals may not fire yet if they ask for one. + +When renewals do arrive, the payload does not distinguish them from a first subscription payment — both are `payment_succeeded` with `mode: "subscription"`. So do not use this trigger for one-time-only actions on a subscription (welcome emails, provisioning, granting access) without your own guard, e.g. an entity record keyed by `provider_customer_id` that the workflow checks and writes. + +app_payment fields live on `.trigger` directly — do NOT use `.trigger.data.*` for this trigger. + +Trigger payload fields (use `${ .trigger.<field> }` in conditions and `do` args): +- `${ .trigger.event_type }` — "payment_succeeded" or "payment_refunded" +- `${ .trigger.provider }` — "stripe", "wix_payments", or "payments_by_wix" +- `${ .trigger.amount }` — integer amount in cents for most currencies (e.g. 2500 = $25.00), matching the platform's payment analytics representation +- `${ .trigger.currency }` — lowercase ISO currency code (e.g. "usd") +- `${ .trigger.is_live }` — false for test/sandbox payments; test payments DO fire this trigger +- `${ .trigger.mode }` — "payment" (one-off checkout) or "subscription" (a subscription charge — first payment OR renewal, indistinguishable); null on refunds. Only Stripe distinguishes these — Wix payments always report "payment" +- `${ .trigger.occurred_at }` — ISO timestamp of the provider event +- `${ .trigger.provider_customer_id }` — provider-side customer/contact ID, or null +- `${ .trigger.provider_transaction_id }` — provider-side transaction/session/refund ID; use it to re-fetch full details (payer email, line items) from a server route via the provider's API. On Stripe refunds this is the CHARGE ID (`ch_...`), not a refund ID — retrieve the charge, which is also where the payer details live +- `${ .trigger.provider_event_id }` — provider webhook event ID (webhook redeliveries are already deduped on it — don't build your own guard) + +The payload carries no payer email or other PII — re-fetch by `provider_transaction_id` from a server route when needed. Zero-amount events (free checkouts, 100% discounts) do not fire. + +Condition examples: +- Only real (live-mode) payments: + `"condition": "${ .trigger.is_live }"` +- Only large payments (over $100): + `"condition": "${ .trigger.amount > 10000 }"` + +## Designing Steps: When to Split vs Combine + +**Split** into separate tasks when operations are independent (a failure shouldn't re-run the others), a delay sits between them, or you want per-step visibility and retry granularity in the run log. **Combine** into one step when operations share setup/auth/transaction, must be atomic, or the second needs the first's full in-memory result. + +**Conditions**: use a `switch` when the outcome picks the path (skip steps, branch, end early — visible in the graph); keep validation/error handling inside the step. Rule of thumb: a flowchart diamond goes in the workflow; an implementation if-statement stays in the step. + +## Important Notes + +- Create the server route BEFORE the workflow that calls it. +- A step calls the server route as the workflow's creator, an app admin, but its URL is public. One only workflows call must set `server: { middleware: [requireUserRoute], handlers }` from `@/lib/auth-middleware`, return 403 unless `context.user.role === "admin"`, then use `context.getBase44().asServiceRole`; never a shared secret. One the app's users also call keeps its usual checks. +- Never put a secret value (API key, token, password) in a workflow — not in step args, conditions or anywhere else. The server route reads the secret from the app's secrets itself. +- Infinite loop prevention: if a workflow has an entity trigger AND creates the same entity type, add a condition to distinguish source entities from workflow-created ones. +- Validation runs automatically on write. Fix errors shown in WORKFLOW ERROR messages and retry. +- Some workflow files carry an `x-base44-migrated-from-automation` marker naming a `legacy_payload_function`. The step calling that function is on a compatibility layer keyed to the function's **name** — while the step calls it, the function receives a legacy input shape. You can change the function's internal logic, but don't change how it consumes its input. To change its inputs or replace it, point the step at a **new** function (different name) written normally — the different name turns the layer off. Other and new steps are normal. + +## Examples + +> Envelope examples below use the builder's `write_file(path, envelope)` form: create the file at `path` with the envelope as its content. + +### "Clean up old data every night at 2am" +1. Add server route: `POST /api/cleanup-old-data` +2. Create workflow: +```json +write_file("base44/workflows/Nightly Cleanup.jsonc", { + "name": "Nightly Cleanup", + "description": "Cleans up old data every night at 2am", + "trigger": { "config": { "trigger_type": "scheduled", "cron_expression": "0 2 * * *", "timezone": "<user_timezone>" } }, + "definition": { + "document": { "dsl": "1.0.0", "name": "nightly_cleanup", "version": "1.0", "namespace": "base44" }, + "do": [{ "cleanup": { "call": "invoke_server_route", "with": { "path": "/api/cleanup-old-data" }, "then": "end" } }] + } +}) +``` + +### "When an order status changes to completed, send a confirmation and wait 3 days, then request a review" +1. Add two server routes: `POST /api/send-confirmation` and `POST /api/request-review` +2. Create workflow: +```json +write_file("base44/workflows/Order Followup.jsonc", { + "name": "Order Followup", + "description": "Sends confirmation on order completion, then requests review after 3 days", + "trigger": { + "config": { "trigger_type": "entity", "entity_name": "orders", "events": ["update"] }, + "condition": "${ .trigger.data.status == \"completed\" and .trigger.old_data.status != \"completed\" }" + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "order_followup", "version": "1.0", "namespace": "base44" }, + "do": [ + { "send_confirmation": { "call": "invoke_server_route", "with": { "path": "/api/send-confirmation", "body": { "order_id": "${ .trigger.entity_id }" } }, "then": "wait_period" } }, + { "wait_period": { "wait": "P3D", "then": "request_review" } }, + { "request_review": { "call": "invoke_server_route", "with": { "path": "/api/request-review", "body": { "order_id": "${ .trigger.entity_id }" } }, "then": "end" } } + ] + } +}) +``` + +### "When someone messages in #general on Slack, log it" +1. Ensure Slack is connected (`list_connectors`, `initiate_connector_connection`) +2. Resolve "#general" to channel ID: create temp function, call Slack API, get "C0123ABC" +3. Add server route: `POST /api/log-slack-message` +4. Create workflow with condition: +```json +write_file("base44/workflows/Slack Logger.jsonc", { + "name": "Slack Logger", + "description": "Logs messages from #general channel", + "trigger": { + "config": { "trigger_type": "connector", "integration_type": "slack", "events": ["message"] }, + "condition": "${ .trigger.data.event.channel == \"C0123ABC\" }" + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "slack_logger", "version": "1.0", "namespace": "base44" }, + "do": [{ "log_message": { "call": "invoke_server_route", "with": { "path": "/api/log-slack-message", "body": { "text": "${ .trigger.data.event.text }", "user": "${ .trigger.data.event.user }" } }, "then": "end" } }] + } +}) +``` + +### "When someone starts a chat with the Support agent, create a ticket and notify the team" +1. Add two server routes: `POST /api/create-support-ticket` and `POST /api/notify-team` +2. Create workflow: +```json +write_file("base44/workflows/Support Ticket.jsonc", { + "name": "Support Ticket", + "description": "Creates a ticket and notifies team when a support conversation starts", + "trigger": { + "config": { "trigger_type": "in_app_agent", "agent_name": "Support", "events": ["conversation_started"] } + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "support_ticket", "version": "1.0", "namespace": "base44" }, + "do": [ + { "create_ticket": { "call": "invoke_server_route", "with": { "path": "/api/create-support-ticket", "body": { "message": "${ .trigger.data.message.text }", "user_id": "${ .trigger.data.app_user.id }" } }, "then": "notify" } }, + { "notify": { "call": "invoke_server_route", "with": { "path": "/api/notify-team", "body": { "ticket_id": "${ .create_ticket.id }", "agent": "${ .trigger.data.event.agent_name }" } }, "then": "end" } } + ] + } +}) +``` + +### "When someone signs up or logs in with a password, record it in the audit log" +1. Add server route: `POST /api/on-password-auth` +2. Create workflow: +```json +write_file("base44/workflows/On Password Auth.jsonc", { + "name": "On Password Auth", + "description": "Records password signups and logins in the audit log", + "trigger": { + "config": { "trigger_type": "app_user_auth", "events": ["login", "signup"] }, + "condition": "${ .trigger.auth_method == \"password\" }" + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "on_password_auth", "version": "1.0", "namespace": "base44" }, + "do": [{ "write_audit": { "call": "invoke_server_route", "with": { "path": "/api/on-password-auth", "body": { "user_id": "${ .trigger.user_id }", "email": "${ .trigger.email }", "activity": "${ .trigger.event_type }" } }, "then": "end" } }] + } +}) +``` + +### "When the app is published for the first time, notify the team" +1. Add server route: `POST /api/on-app-launched` +2. Create workflow: +```json +write_file("base44/workflows/On First Publish.jsonc", { + "name": "On First Publish", + "description": "Notifies the team when the app is published for the first time", + "trigger": { + "config": { "trigger_type": "app_publish", "events": ["published"] }, + "condition": "${ .trigger.is_first_publish }" + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "on_first_publish", "version": "1.0", "namespace": "base44" }, + "do": [{ "notify_team": { "call": "invoke_server_route", "with": { "path": "/api/on-app-launched", "body": { "published_at": "${ .trigger.occurred_at }", "visibility": "${ .trigger.visibility }" } }, "then": "end" } }] + } +}) +``` + +### "When someone pays, send a thank-you email" +1. Add server route: `POST /api/send-payment-thank-you` — it looks up the payer's email from the provider API using the transaction ID, then sends the email +2. Create workflow: +```json +write_file("base44/workflows/On Payment.jsonc", { + "name": "On Payment", + "description": "Sends a thank-you email when a live payment succeeds", + "trigger": { + "config": { "trigger_type": "app_payment", "events": ["payment_succeeded"] }, + "condition": "${ .trigger.is_live }" + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "on_payment", "version": "1.0", "namespace": "base44" }, + "do": [{ "send_thank_you": { "call": "invoke_server_route", "with": { "path": "/api/send-payment-thank-you", "body": { "transaction_id": "${ .trigger.provider_transaction_id }", "amount": "${ .trigger.amount }", "currency": "${ .trigger.currency }" } }, "then": "end" } }] + } +}) +``` From f42759f020f6d46d253927f4c96c9f2d7f696de6 Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Tue, 20 Oct 2026 08:06:50 +0000 Subject: [PATCH 02/15] chore(base44-platform): recompile with every rolled-out workflow trigger and activity Pinned to the apper commit that sets the workflow rollout payloads to `all`, matching production. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLKGecSgMqR14kYQTrugoR --- APPER_COMMIT | 2 +- skills/base44-platform-manifest.json | 4 +- .../references/web-spa.md | 96 +++++++++++++++++++ .../references/web-tanstack.md | 96 +++++++++++++++++++ 4 files changed, 195 insertions(+), 3 deletions(-) diff --git a/APPER_COMMIT b/APPER_COMMIT index a80d41f..d935c32 100644 --- a/APPER_COMMIT +++ b/APPER_COMMIT @@ -1 +1 @@ -a513e8922412ce0b28bbe6e762108f29fbdb2b56 +9f85a846f05a0446afce28698b6f500d935914a2 diff --git a/skills/base44-platform-manifest.json b/skills/base44-platform-manifest.json index 3d8cfa8..ee63d45 100644 --- a/skills/base44-platform-manifest.json +++ b/skills/base44-platform-manifest.json @@ -76,7 +76,7 @@ "skills/base44-platform-workflows/SKILL.md": "55fb8de2004bbbcb4369ac58b91acd5613c8b750eed79e4dcc7b657e78891284", "skills/base44-platform-workflows/references/automations-legacy.md": "df52bfdc18a3e7b886c169224edb68479771efd2f71eebd2652f43482caf10bc", "skills/base44-platform-workflows/references/mobile-automations.md": "7ab2c859ac0ad6f531ae0918a7bf125e5611af76e7905693c0f78c8e2c83799e", - "skills/base44-platform-workflows/references/web-spa.md": "d4bed7815f168037a52626e3fb83837bf530914398b642349f75c2e0193c06a3", - "skills/base44-platform-workflows/references/web-tanstack.md": "b9c50670cbd7bf27eb184796a330f7603b5a92cebbbba386f2fef792f6961ec2" + "skills/base44-platform-workflows/references/web-spa.md": "997a3deeb85374196289271a0586783779255811a3573a11dfc4fe45155a89bf", + "skills/base44-platform-workflows/references/web-tanstack.md": "75e7fd03e78a8615b99cd274fcb925cc36866dcd023fbd8cc5ce150ec3f35a3e" } } diff --git a/skills/base44-platform-workflows/references/web-spa.md b/skills/base44-platform-workflows/references/web-spa.md index b27543b..198c445 100644 --- a/skills/base44-platform-workflows/references/web-spa.md +++ b/skills/base44-platform-workflows/references/web-spa.md @@ -22,10 +22,14 @@ What workflows can be built from; the authoring guide that follows documents eac - app_user_auth — Fires when an app user signs up or logs in. - app_publish — Fires when the app is published from the builder. - app_payment — Fires when a payment succeeds or is refunded in the app's payment provider. +- webhook — Fires on an inbound POST to a reusable workspace webhook URL, from any source. Use it for a third-party event no connector trigger covers - don't connect that provider or poll it to get the event. ### Activities (what a `call` step can do) +- app_security_scan — Security scan (same as the Security tab): `start` kicks it off, `results` reads the latest findings. - compute_seconds_until — Computes the seconds from now until a target datetime — feed the result into a dynamic wait. - invoke_backend_function — Runs a deployed backend function — your code for API calls, data transforms, and entity CRUD. +- publish_social_post — Publishes a scheduled post to the app's connected LinkedIn, Instagram or Facebook account. +- simulate_social_post — Simulate publishing a preview-only social post without contacting a social platform. ### Task Types (how steps are structured) - call — invoke a registered activity with `with` args, then transition via `then`. @@ -127,6 +131,14 @@ When editing, preserve `x-base44` where the task's purpose is unchanged; update ## Available Activities +### app_security_scan +The app's security scan — the same scan as the builder's Security tab (RLS, secrets, backend functions, dependencies, static code). `action: start` starts a scan and returns at once, without findings. `action: results` reads the latest completed scan. +- When to use: Whenever a workflow needs the app's security posture or findings, on any trigger — to refresh them (`start`) or to read and act on them (`results`). A scan runs in the background for up to ~10 minutes, so `start` never returns findings: `wait` at least PT10M, then `results` passing `after: ${ .<start step>.requested_at }` so an older scan is reported as `pending`, not as ready. If `results` says `running`, wait PT5M and read once more. `results` alone is enough when the latest scan will do. Branch on `state` — only `ready` carries `scan`. Zero findings means clean only when `scan.status` is `up_to_date` and `scan.rls_scan_failed` is false. `scan.findings` is capped: severity-first, 50 by default — pass `max_findings` (up to 100) to raise it, check `findings_truncated`, and take the count from `total_findings`, never from the list length. If nothing changed since the last scan, `start` itself returns `ready` with that scan. Run now behaves like any other run: `start` really starts a scan. +- `action`: `start` to start a scan, or `results` to read the latest completed scan. Required. +- `after`: results only, optional. ISO-8601 datetime; a scan completed before it is reported as `pending` rather than `ready`. Pass the `requested_at` returned by the `start` step. +- `max_findings`: results only, optional. How many findings to include in `scan.findings`, severity-first (default 50, max 100); `scan.findings_truncated` says whether any were left out. +- Returns: {"action": "start" | "results", "state": str, "scan": {...} | null, "requested_at": str (start only; the `after` for a follow-up results — when state is "ready" it is the returned scan's own scanned_at, so results accepts it)}. start states: "started" (scan spawned), "running" (a scan is already in flight), "ready" (nothing changed since the last scan; `scan` is current). results states: "ready" (`scan` set), "running", "pending" (latest scan predates `after`), "failed" (last scan failed), "not_scanned". `scan` is set only when state is "ready": {"scanned_at": str, "status": "up_to_date" | "out_of_date" (as the Security tab shows it), "rls_scan_failed": bool (RLS section did not complete — empty RLS findings are not a clean result), "risk_level": str, "total_findings": int, "counts_by_severity": {severity: int}, "counts_by_category": {category: {severity: int}}, "finding_categories": [str], "findings": [{"category": "rls" | "secrets" | "backend_functions" | "dependencies" | "code" | "core_integration", "severity": str, "title": str, "description": str, "file_path"?: str, "line"?: int, "recommendation"?: str, "package_name"?: str, "current_version"?: str, "fixed_version"?: str, "vulnerability_id"?: str}], "findings_truncated": bool}. `start` never returns findings (its `scan.findings` is empty). + ### compute_seconds_until Compute seconds from now until a target datetime. - `target_iso`: ISO 8601 datetime string @@ -139,6 +151,21 @@ Call a deployed Deno backend function. - `args`: Dict of arguments passed as the function request body (supports jq expressions) - Returns: Parsed JSON response from the function +### publish_social_post +Publish a scheduled social post to the app's connected LinkedIn, Instagram or Facebook account. Takes the post created by create_scheduled_social_post. +- When to use: Posting to the app owner's connected social account on a schedule. Two steps, in order: call create_scheduled_social_post to store the content and get a scheduled_post_id, then author a workflow that passes that id here. The post is the source of truth for what gets published; the workflow's trigger decides when. Requires an active connector for the platform — create_scheduled_social_post checks and will tell you if the user needs to connect one first. Run now never publishes: it reports what would happen and leaves the post untouched. +- `scheduled_post_id`: ID returned by create_scheduled_social_post. Must belong to this app. +- Returns: {"scheduled_post_id": str, "status": str, "already_posted": bool, "external_post_id": str | null, "external_post_url": str | null, "dry_run": bool} + +### simulate_social_post +Simulate publishing a preview-only social post without contacting a social platform. +- `scheduled_post_id`: ID returned by create_scheduled_social_post. Must belong to this app. +- Returns: {"scheduled_post_id": str, "status": str, "already_posted": bool, "external_post_id": str | null, "external_post_url": str | null, "dry_run": bool} + +## Choosing Between Activities + +See each activity's "When to use" line above for what it's best at. + ## jq Expression Language Use `${ .field }` syntax (CNCF SWF v1.0). @@ -392,6 +419,49 @@ Condition examples: - Only large payments (over $100): `"condition": "${ .trigger.amount > 10000 }"` +### webhook + +A workspace webhook must already exist — this trigger listens on one, it does +not create one. Use `select_or_create_workflow_webhook` to pick an existing +listener or make a new one, and write the id it returns here. Reuse an existing +one where you can: many providers allow only a single webhook URL per account, +so a second listener for the same provider usually cannot be wired up at all. + +```json +{ "trigger_type": "webhook", "webhook_id": "<id from the tool>", + "webhook_name": "hubspot-events", "events": ["contact.creation"] } +``` + +`webhook_id` is the reference — never a URL and never a token. You cannot guess +or invent it: it only exists once a tool hands it to you, and an id that is not +in this workspace blocks activation with `WEBHOOK_NOT_FOUND`. + +`webhook_name` is the listener's display name at authoring time. It is carried for +readability only and is never resolved against — the user may rename the +listener, which changes the label and nothing else. + +One webhook URL commonly carries several event types. `events` filters which +ones run this workflow; an empty list subscribes to all of them. Reusing a +webhook across workflows is normal and preferred — many providers allow only +one webhook URL per account. Only set `events` when the webhook classifies +event types; otherwise activation fails with `WEBHOOK_HAS_NO_EVENT_TYPES`. + +Trigger payload (use `${ .trigger.* }` in conditions and `do` args): +- `${ .trigger.data.<field> }` — the parsed request body, e.g. `${ .trigger.data.properties.email }` +- `${ .trigger.event_type }` — the event type this request was classified as +- `${ .trigger.webhook_name }` — the webhook's display name (may have changed since authoring) +- `${ .trigger.webhook_id }` — the webhook's id, stable across renames +- `${ .trigger.headers.<name> }` — request headers, lowercased, credentials removed +- `${ .trigger.query.<name> }` — query-string params, credentials removed + +`${ .trigger.data }` arrives as `null` when the body was too large to forward. +Unlike an entity trigger there is nothing to re-fetch — the body is never +persisted — so a workflow that must tolerate big payloads should branch on the +absence rather than assume the field is there. + +Condition example — only enterprise-tier contacts: +`"condition": "${ .trigger.data.properties.tier == \"enterprise\" }"` + ## Designing Steps: When to Split vs Combine **Split** into separate tasks when operations are independent (a failure shouldn't re-run the others), a delay sits between them, or you want per-step visibility and retry granularity in the run log. **Combine** into one step when operations share setup/auth/transaction, must be atomic, or the second needs the first's full in-memory result. @@ -541,3 +611,29 @@ write_file("base44/workflows/On Payment.jsonc", { } }) ``` + +### Refresh, then act on the findings — the start / wait / results pattern (any trigger) +Point the `ready` branch at whatever step consumes `${ .read.scan }` (its `findings`, `total_findings`, `risk_level`). +```json +"do": [ + { "scan": { "call": "app_security_scan", "with": { "action": "start" }, "then": "settle" } }, + { "settle": { "wait": "PT15M", "then": "read" } }, + { "read": { "call": "app_security_scan", "with": { "action": "results", "after": "${ .scan.requested_at }" }, "then": "route" } }, + { "route": { "switch": [ + { "ready": { "when": "${ .read.state == \"ready\" }", "then": "end" } }, + { "running": { "when": "${ .read.state == \"running\" }", "then": "settle_again" } }, + { "other": { "then": "end" } } + ] } }, + { "settle_again": { "wait": "PT5M", "then": "read_again" } }, + { "read_again": { "call": "app_security_scan", "with": { "action": "results", "after": "${ .scan.requested_at }" }, "then": "end" } } +] +``` + +### "Post our launch announcement to LinkedIn on Tuesday morning" — the two-step pattern +1. Call `create_scheduled_social_post` with the platform, copy, and time. It verifies the LinkedIn connector and returns a `scheduled_post_id`. +2. Author a workflow on a schedule trigger set to that time, passing the returned id: +```json +"do": [ + { "publish": { "call": "publish_social_post", "with": { "scheduled_post_id": "sp_a1b2c3" }, "then": "end" } } +] +``` diff --git a/skills/base44-platform-workflows/references/web-tanstack.md b/skills/base44-platform-workflows/references/web-tanstack.md index a786aa9..7caf66b 100644 --- a/skills/base44-platform-workflows/references/web-tanstack.md +++ b/skills/base44-platform-workflows/references/web-tanstack.md @@ -22,10 +22,14 @@ What workflows can be built from; the authoring guide that follows documents eac - app_user_auth — Fires when an app user signs up or logs in. - app_publish — Fires when the app is published from the builder. - app_payment — Fires when a payment succeeds or is refunded in the app's payment provider. +- webhook — Fires on an inbound POST to a reusable workspace webhook URL, from any source. Use it for a third-party event no connector trigger covers - don't connect that provider or poll it to get the event. ### Activities (what a `call` step can do) +- app_security_scan — Security scan (same as the Security tab): `start` kicks it off, `results` reads the latest findings. - compute_seconds_until — Computes the seconds from now until a target datetime — feed the result into a dynamic wait. - invoke_server_route — Calls a route on the app's own server. +- publish_social_post — Publishes a scheduled post to the app's connected LinkedIn, Instagram or Facebook account. +- simulate_social_post — Simulate publishing a preview-only social post without contacting a social platform. ### Task Types (how steps are structured) - call — invoke a registered activity with `with` args, then transition via `then`. @@ -127,6 +131,14 @@ When editing, preserve `x-base44` where the task's purpose is unchanged; update ## Available Activities +### app_security_scan +The app's security scan — the same scan as the builder's Security tab (RLS, secrets, backend functions, dependencies, static code). `action: start` starts a scan and returns at once, without findings. `action: results` reads the latest completed scan. +- When to use: Whenever a workflow needs the app's security posture or findings, on any trigger — to refresh them (`start`) or to read and act on them (`results`). A scan runs in the background for up to ~10 minutes, so `start` never returns findings: `wait` at least PT10M, then `results` passing `after: ${ .<start step>.requested_at }` so an older scan is reported as `pending`, not as ready. If `results` says `running`, wait PT5M and read once more. `results` alone is enough when the latest scan will do. Branch on `state` — only `ready` carries `scan`. Zero findings means clean only when `scan.status` is `up_to_date` and `scan.rls_scan_failed` is false. `scan.findings` is capped: severity-first, 50 by default — pass `max_findings` (up to 100) to raise it, check `findings_truncated`, and take the count from `total_findings`, never from the list length. If nothing changed since the last scan, `start` itself returns `ready` with that scan. Run now behaves like any other run: `start` really starts a scan. +- `action`: `start` to start a scan, or `results` to read the latest completed scan. Required. +- `after`: results only, optional. ISO-8601 datetime; a scan completed before it is reported as `pending` rather than `ready`. Pass the `requested_at` returned by the `start` step. +- `max_findings`: results only, optional. How many findings to include in `scan.findings`, severity-first (default 50, max 100); `scan.findings_truncated` says whether any were left out. +- Returns: {"action": "start" | "results", "state": str, "scan": {...} | null, "requested_at": str (start only; the `after` for a follow-up results — when state is "ready" it is the returned scan's own scanned_at, so results accepts it)}. start states: "started" (scan spawned), "running" (a scan is already in flight), "ready" (nothing changed since the last scan; `scan` is current). results states: "ready" (`scan` set), "running", "pending" (latest scan predates `after`), "failed" (last scan failed), "not_scanned". `scan` is set only when state is "ready": {"scanned_at": str, "status": "up_to_date" | "out_of_date" (as the Security tab shows it), "rls_scan_failed": bool (RLS section did not complete — empty RLS findings are not a clean result), "risk_level": str, "total_findings": int, "counts_by_severity": {severity: int}, "counts_by_category": {category: {severity: int}}, "finding_categories": [str], "findings": [{"category": "rls" | "secrets" | "backend_functions" | "dependencies" | "code" | "core_integration", "severity": str, "title": str, "description": str, "file_path"?: str, "line"?: int, "recommendation"?: str, "package_name"?: str, "current_version"?: str, "fixed_version"?: str, "vulnerability_id"?: str}], "findings_truncated": bool}. `start` never returns findings (its `scan.findings` is empty). + ### compute_seconds_until Compute seconds from now until a target datetime. - `target_iso`: ISO 8601 datetime string @@ -141,6 +153,21 @@ Call a route served by the app's own full-stack server. - `query`: Dict of query-string parameters (supports jq expressions) - Returns: Parsed JSON response from the route +### publish_social_post +Publish a scheduled social post to the app's connected LinkedIn, Instagram or Facebook account. Takes the post created by create_scheduled_social_post. +- When to use: Posting to the app owner's connected social account on a schedule. Two steps, in order: call create_scheduled_social_post to store the content and get a scheduled_post_id, then author a workflow that passes that id here. The post is the source of truth for what gets published; the workflow's trigger decides when. Requires an active connector for the platform — create_scheduled_social_post checks and will tell you if the user needs to connect one first. Run now never publishes: it reports what would happen and leaves the post untouched. +- `scheduled_post_id`: ID returned by create_scheduled_social_post. Must belong to this app. +- Returns: {"scheduled_post_id": str, "status": str, "already_posted": bool, "external_post_id": str | null, "external_post_url": str | null, "dry_run": bool} + +### simulate_social_post +Simulate publishing a preview-only social post without contacting a social platform. +- `scheduled_post_id`: ID returned by create_scheduled_social_post. Must belong to this app. +- Returns: {"scheduled_post_id": str, "status": str, "already_posted": bool, "external_post_id": str | null, "external_post_url": str | null, "dry_run": bool} + +## Choosing Between Activities + +See each activity's "When to use" line above for what it's best at. + ## jq Expression Language Use `${ .field }` syntax (CNCF SWF v1.0). @@ -394,6 +421,49 @@ Condition examples: - Only large payments (over $100): `"condition": "${ .trigger.amount > 10000 }"` +### webhook + +A workspace webhook must already exist — this trigger listens on one, it does +not create one. Use `select_or_create_workflow_webhook` to pick an existing +listener or make a new one, and write the id it returns here. Reuse an existing +one where you can: many providers allow only a single webhook URL per account, +so a second listener for the same provider usually cannot be wired up at all. + +```json +{ "trigger_type": "webhook", "webhook_id": "<id from the tool>", + "webhook_name": "hubspot-events", "events": ["contact.creation"] } +``` + +`webhook_id` is the reference — never a URL and never a token. You cannot guess +or invent it: it only exists once a tool hands it to you, and an id that is not +in this workspace blocks activation with `WEBHOOK_NOT_FOUND`. + +`webhook_name` is the listener's display name at authoring time. It is carried for +readability only and is never resolved against — the user may rename the +listener, which changes the label and nothing else. + +One webhook URL commonly carries several event types. `events` filters which +ones run this workflow; an empty list subscribes to all of them. Reusing a +webhook across workflows is normal and preferred — many providers allow only +one webhook URL per account. Only set `events` when the webhook classifies +event types; otherwise activation fails with `WEBHOOK_HAS_NO_EVENT_TYPES`. + +Trigger payload (use `${ .trigger.* }` in conditions and `do` args): +- `${ .trigger.data.<field> }` — the parsed request body, e.g. `${ .trigger.data.properties.email }` +- `${ .trigger.event_type }` — the event type this request was classified as +- `${ .trigger.webhook_name }` — the webhook's display name (may have changed since authoring) +- `${ .trigger.webhook_id }` — the webhook's id, stable across renames +- `${ .trigger.headers.<name> }` — request headers, lowercased, credentials removed +- `${ .trigger.query.<name> }` — query-string params, credentials removed + +`${ .trigger.data }` arrives as `null` when the body was too large to forward. +Unlike an entity trigger there is nothing to re-fetch — the body is never +persisted — so a workflow that must tolerate big payloads should branch on the +absence rather than assume the field is there. + +Condition example — only enterprise-tier contacts: +`"condition": "${ .trigger.data.properties.tier == \"enterprise\" }"` + ## Designing Steps: When to Split vs Combine **Split** into separate tasks when operations are independent (a failure shouldn't re-run the others), a delay sits between them, or you want per-step visibility and retry granularity in the run log. **Combine** into one step when operations share setup/auth/transaction, must be atomic, or the second needs the first's full in-memory result. @@ -543,3 +613,29 @@ write_file("base44/workflows/On Payment.jsonc", { } }) ``` + +### Refresh, then act on the findings — the start / wait / results pattern (any trigger) +Point the `ready` branch at whatever step consumes `${ .read.scan }` (its `findings`, `total_findings`, `risk_level`). +```json +"do": [ + { "scan": { "call": "app_security_scan", "with": { "action": "start" }, "then": "settle" } }, + { "settle": { "wait": "PT15M", "then": "read" } }, + { "read": { "call": "app_security_scan", "with": { "action": "results", "after": "${ .scan.requested_at }" }, "then": "route" } }, + { "route": { "switch": [ + { "ready": { "when": "${ .read.state == \"ready\" }", "then": "end" } }, + { "running": { "when": "${ .read.state == \"running\" }", "then": "settle_again" } }, + { "other": { "then": "end" } } + ] } }, + { "settle_again": { "wait": "PT5M", "then": "read_again" } }, + { "read_again": { "call": "app_security_scan", "with": { "action": "results", "after": "${ .scan.requested_at }" }, "then": "end" } } +] +``` + +### "Post our launch announcement to LinkedIn on Tuesday morning" — the two-step pattern +1. Call `create_scheduled_social_post` with the platform, copy, and time. It verifies the LinkedIn connector and returns a `scheduled_post_id`. +2. Author a workflow on a schedule trigger set to that time, passing the returned id: +```json +"do": [ + { "publish": { "call": "publish_social_post", "with": { "scheduled_post_id": "sp_a1b2c3" }, "then": "end" } } +] +``` From a89c76a0e389cc21923e3a329c32855ed753ae6c Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Tue, 20 Oct 2026 08:36:03 +0000 Subject: [PATCH 03/15] chore(base44-platform): recompile after apper merged the v2 entities SDK Main folded the paginated / count / aggregate / upsert entities SDK into the default prompt, so the compiled entities references now teach it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLKGecSgMqR14kYQTrugoR --- APPER_COMMIT | 2 +- .../references/web.md | 23 ++++-- skills/base44-platform-manifest.json | 6 +- .../references/web-spa.md | 78 ++++++++++++++++--- .../references/web-tanstack.md | 78 ++++++++++++++++--- 5 files changed, 154 insertions(+), 33 deletions(-) diff --git a/APPER_COMMIT b/APPER_COMMIT index d935c32..47573e7 100644 --- a/APPER_COMMIT +++ b/APPER_COMMIT @@ -1 +1 @@ -9f85a846f05a0446afce28698b6f500d935914a2 +5712fcc95b9241f84ec55318763f7b0ee5d2df2a diff --git a/skills/base44-platform-entities/references/web.md b/skills/base44-platform-entities/references/web.md index 9f2576a..49e0a15 100644 --- a/skills/base44-platform-entities/references/web.md +++ b/skills/base44-platform-entities/references/web.md @@ -17,14 +17,27 @@ Never store large content (base64, PDFs, blobs) in entity fields — upload via </field_limits> <entities_sdk> -Entities SDK on the pre-initialized base44 client; each entity is base44.entities.EntityName: +Entities SDK on the pre-initialized base44 client; each entity is base44.entities.EntityName. Calls are rate-limited per app and every record read counts against the app's budget, so the server does the filtering, counting and math. Pick the call by what the screen needs: +- a list of records → filter(query, {sort, limit, fields}), a page: read .items, never treat it as an array; more rows on demand → the same call with cursor +- a number (total, badge, "N of M") → count(query); a sum or average → aggregate +- totals per group, a chart, a leaderboard, "top products" → aggregate({groupBy or dateBucket, sum/avg/min/max}) +- dropdown or autocomplete values → the schema enum when the field declares one, else filter(query, {distinct: 'field'}) +- import or sync keyed by your own id → upsert(records, {key}) +- every record → only for an export (CSV download, backup, sync), with the cursor loop +A list on screen is one page: filter(query, {sort, limit: 50}) with a "load more" that passes cursor, not a 500-row fetch into state. A number or grouping added to an existing page gets its own count/aggregate call even when a list is already loaded — that list is one page, not the data — and replacing a loaded-list computation with the server call is part of the ask, not a refactor. Every filter, dropdown choice or search term the user changes is a new filter() call with those conditions in the query (text search: $regex), replacing the page on screen; narrowing the loaded page in JS shows only what happened to be on it. Never load records to filter, count, sum or dedupe them in JS — `const all = await Sale.list(); all.filter(s => s.store === store).reduce(...)` is wrong even for a small table, because tables grow; `Sale.aggregate({query: {store}, sum: 'total'})` is right. A search box is a `$regex` filter on the server, not `.includes()` on a loaded list. ```js import { base44 } from '@/api/base44Client'; -base44.entities.Todo.list() -base44.entities.Todo.list('-updated_date', 20) // sort, limit -base44.entities.Todo.filter({status: 'active', created_by_id: user.id}, '-created_date', 10) +base44.entities.Todo.filter({priority: {$in: ['high', 'urgent']}, due_date: {$gte: '2026-01-01', $lt: '2026-02-01'}, archived: {$exists: false}}, {sort: '-due_date', limit: 100, fields: ['title', 'due_date']}) // → {items, next_cursor, has_more}: the options form ALWAYS returns a page object, never an array — read .items. MongoDB operators ($in/$nin/$gt/$gte/$lt/$lte/$ne/$exists/$or) on any field; fields returns only those (plus id) +base44.entities.Todo.filter({status: 'open'}, {sort: '-created_date', limit: 1000, cursor}) // one page: {items, next_cursor, has_more}. "Load more"/infinite scroll and export loops: first call without cursor, then cursor = page.next_cursor until has_more is false. Same options on list({sort, limit, cursor, fields}) +base44.entities.Todo.count({status: 'open'}) // a number, no records transferred: totals, badges, "N of M", pagination totals +// An entity with field-level read rules (per-field visibility in its schema) rejects aggregate and distinct with a 400: there, use count for numbers and one filter page for the rest, and say so in the summary +base44.entities.Todo.aggregate({query: {status: 'done'}, groupBy: 'owner', sum: ['amount', 'hours'], sort: '-sum_amount'}) // {rows: [{owner, count, sum_amount, sum_hours}], truncated}; one call computes every sum/avg/min/max you need; sort names a group field or a computed column (count, sum_<field>, avg_<field>…), nothing else. Also dateBucket: {field: 'created_date', unit: 'day'|'week'|'month'|'year'}, avg/min/max, countDistinct: 'field', having: {count: {$gt: 1}} (duplicates), limit up to 1000. Dashboards, totals per group, charts +// aggregate has the lowest rate limit (about 15 calls a minute per app): still far better than loading the collection to compute in JS, but spend it sparingly: several counters or totals on the same entity (per status, per owner, open vs done) come from one aggregate with groupBy, each read from its row, not one aggregate or count call per counter; a separate call only for another entity, another query, or a measure one call cannot hold (a second countDistinct) +base44.entities.Todo.list({distinct: 'category'}) // {items: values, next_cursor, has_more}: dropdowns and autocomplete; also filter(query, {distinct: 'field'}) +base44.entities.Todo.upsert(records, {key: 'external_id'}) // create-or-update by your own key (or {key: ['a', 'b']}), up to 500 per call: {created, updated, records}. Imports and syncs; never read first to check what exists +base44.entities.Todo.filter({name: {$regex: '^ann', $options: 'i'}}) // prefix/contains text match, only when the feature really needs it: it scans, so keep it behind another filter, keep the pattern simple, and never use it for exact matches base44.entities.Todo.create({title: "Todo 1"}) base44.entities.Todo.bulkCreate([{title: "Todo 2"}, {title: "Todo 3"}]) base44.entities.Todo.update(todo.id, {description: "new"}) @@ -35,7 +48,7 @@ base44.entities.Todo.deleteMany({status: "archived"}) // deletes all matches in ``` <user_and_auth> -Built-in User entity uses the same SDK for reads (await base44.entities.User.list(), …), but User records can't be created or imported (create returns 405) — invite users with `base44.users.inviteUser(email, role)`. Auth SDK: +Built-in User entity uses the same SDK for reads (await base44.entities.User.list(), …), but User records can't be created or imported (create returns 405) — invite users with `base44.users.inviteUser(email, role)`. count, aggregate, upsert and the cursor form are not available on User. Auth SDK: - await base44.auth.me() — current user; can throw if not logged in on a public app. - await base44.auth.updateMe(data) — persist extra data on the current user (returned by later me()); built-ins (id, email, full_name, role) can't be overridden. - base44.auth.isAuthenticated() — Promise<boolean>. diff --git a/skills/base44-platform-manifest.json b/skills/base44-platform-manifest.json index ee63d45..022435e 100644 --- a/skills/base44-platform-manifest.json +++ b/skills/base44-platform-manifest.json @@ -17,7 +17,7 @@ "skills/base44-platform-entities/SKILL.md": "c84de4b41d8d64b20e4a4522a97422f500eb3554e9ccc48075130fdb8ae90d06", "skills/base44-platform-entities/references/mobile-and-games.md": "559313916e24dcfb4cc9ba2e250d8c449ac8555ce8e5494d91d8075b86aa1b0d", "skills/base44-platform-entities/references/rls.md": "d70c3f6aee3f9d4bb4a771078cd70e1c2ddfffa84a4598627b36c13f490e77b0", - "skills/base44-platform-entities/references/web.md": "bd288ee846dedd4e908f93a03291a12a4d26dc4877db46c92d05758d7ef15934", + "skills/base44-platform-entities/references/web.md": "02f81b350c864455c722c13405cceabdb5b504de396f272aa1e51571a3cf8a01", "skills/base44-platform-frontend/SKILL.md": "40f9751943bcaac870313afa6b6335bd9d05908a7cee1f82be454f2ada38f8f0", "skills/base44-platform-frontend/references/mobile.md": "68d1983605b40f812ee6ed6374649763f469681fd8cfd5f198a3e39e26107957", "skills/base44-platform-frontend/references/web-spa.md": "a019c49df78cbd7f4b458992fc2a6483f10230c8a2f475dc34c035b3af38ae3e", @@ -76,7 +76,7 @@ "skills/base44-platform-workflows/SKILL.md": "55fb8de2004bbbcb4369ac58b91acd5613c8b750eed79e4dcc7b657e78891284", "skills/base44-platform-workflows/references/automations-legacy.md": "df52bfdc18a3e7b886c169224edb68479771efd2f71eebd2652f43482caf10bc", "skills/base44-platform-workflows/references/mobile-automations.md": "7ab2c859ac0ad6f531ae0918a7bf125e5611af76e7905693c0f78c8e2c83799e", - "skills/base44-platform-workflows/references/web-spa.md": "997a3deeb85374196289271a0586783779255811a3573a11dfc4fe45155a89bf", - "skills/base44-platform-workflows/references/web-tanstack.md": "75e7fd03e78a8615b99cd274fcb925cc36866dcd023fbd8cc5ce150ec3f35a3e" + "skills/base44-platform-workflows/references/web-spa.md": "ba6b6f4165cfbb9ce7297ed7568ac515112038f5a2eae38f3ab29b5a4557aed0", + "skills/base44-platform-workflows/references/web-tanstack.md": "a91a7cb530031380439ca2a8779069f70458efc32082c58fd01e54e3aaf2bb3c" } } diff --git a/skills/base44-platform-workflows/references/web-spa.md b/skills/base44-platform-workflows/references/web-spa.md index 198c445..050338d 100644 --- a/skills/base44-platform-workflows/references/web-spa.md +++ b/skills/base44-platform-workflows/references/web-spa.md @@ -21,8 +21,9 @@ What workflows can be built from; the authoring guide that follows documents eac - in_app_agent — Fires when a conversation starts with an in-app agent. - app_user_auth — Fires when an app user signs up or logs in. - app_publish — Fires when the app is published from the builder. -- app_payment — Fires when a payment succeeds or is refunded in the app's payment provider. +- app_payment — Fires when a payment succeeds or is refunded, or a subscription is canceled, in the app's payment provider. - webhook — Fires on an inbound POST to a reusable workspace webhook URL, from any source. Use it for a third-party event no connector trigger covers - don't connect that provider or poll it to get the event. +- production_issue — Fires when a production issue the owner set an alert on happens again or crosses a threshold. ### Activities (what a `call` step can do) - app_security_scan — Security scan (same as the Security tab): `start` kicks it off, `results` reads the latest findings. @@ -389,29 +390,33 @@ Condition examples: { "trigger_type": "app_payment", "events": ["payment_succeeded"] } ``` -Requires a connected payment provider (Stripe, Wix Payments, or Payments by Wix) — activating a workflow with this trigger fails when none is connected. Connect a provider first, then create the workflow. The payload is normalized across providers; workflows never receive provider-shaped raw data. +Requires a connected payment provider (Stripe, PayPal, Wix Payments, or Payments by Wix): activating a workflow with this trigger fails when none is connected. Connect a provider first, then create the workflow. The payload is normalized across providers; workflows never receive provider-shaped raw data. -Valid events: `payment_succeeded` (a successful payment — a completed checkout and the first payment of a subscription), `payment_refunded` (an approved refund). Failed payment attempts do NOT fire this trigger. +Valid events: `payment_succeeded` (a successful payment: a completed checkout and the first payment of a subscription), `payment_refunded` (an approved refund, including one made from the Payments dashboard or the provider's own site), `subscription_canceled` (a subscription stopped billing: canceled or ended on Stripe and PayPal, and paused/suspended on PayPal only, e.g. by the buyer in PayPal or after failed payments; a Stripe pause fires nothing; a PayPal subscription resumed outside the app fires nothing and `listPurchases` can't show it, so tell the user to restore that access by hand). Failed payment attempts do NOT fire this trigger. -Recurring subscription renewal charges are wired to fire `payment_succeeded` too, but delivery is not enabled yet for existing Stripe accounts (it needs a Stripe app permission + per-account re-consent). Do not build a workflow whose only purpose is reacting to renewals, and tell the user renewals may not fire yet if they ask for one. +Use `payment_refunded` and `subscription_canceled` to revoke what a purchase granted: match the event to the purchase the app saved by whichever of `subscription_id`, `checkout_id` and `payment_id` it carries, and make the revoke idempotent. + +Recurring subscription renewal charges are wired to fire `payment_succeeded` too, but delivery is not enabled yet for existing Stripe accounts (it needs a Stripe app permission + per-account re-consent). Do not build a workflow whose only purpose is reacting to renewals, and tell the user renewals may not fire yet if they ask for one. The same holds for `subscription_canceled` on Stripe; on PayPal both fire. When renewals do arrive, the payload does not distinguish them from a first subscription payment — both are `payment_succeeded` with `mode: "subscription"`. So do not use this trigger for one-time-only actions on a subscription (welcome emails, provisioning, granting access) without your own guard, e.g. an entity record keyed by `provider_customer_id` that the workflow checks and writes. app_payment fields live on `.trigger` directly — do NOT use `.trigger.data.*` for this trigger. Trigger payload fields (use `${ .trigger.<field> }` in conditions and `do` args): -- `${ .trigger.event_type }` — "payment_succeeded" or "payment_refunded" -- `${ .trigger.provider }` — "stripe", "wix_payments", or "payments_by_wix" -- `${ .trigger.amount }` — integer amount in cents for most currencies (e.g. 2500 = $25.00), matching the platform's payment analytics representation +- `${ .trigger.event_type }`: "payment_succeeded", "payment_refunded" or "subscription_canceled" +- `${ .trigger.provider }`: "stripe", "paypal", "wix_payments", or "payments_by_wix" +- `${ .trigger.amount }`: integer amount in cents for most currencies (e.g. 2500 = $25.00), matching the platform's payment analytics representation; 0 on `subscription_canceled` - `${ .trigger.currency }` — lowercase ISO currency code (e.g. "usd") - `${ .trigger.is_live }` — false for test/sandbox payments; test payments DO fire this trigger -- `${ .trigger.mode }` — "payment" (one-off checkout) or "subscription" (a subscription charge — first payment OR renewal, indistinguishable); null on refunds. Only Stripe distinguishes these — Wix payments always report "payment" +- `${ .trigger.mode }`: "payment" (one-off checkout) or "subscription" (a subscription charge, first payment OR renewal, indistinguishable); null on refunds. Only Stripe and PayPal distinguish these; Wix payments always report "payment" - `${ .trigger.occurred_at }` — ISO timestamp of the provider event - `${ .trigger.provider_customer_id }` — provider-side customer/contact ID, or null -- `${ .trigger.provider_transaction_id }` — provider-side transaction/session/refund ID; use it to re-fetch full details (payer email, line items) from a backend function via the provider's API. On Stripe refunds this is the CHARGE ID (`ch_...`), not a refund ID — retrieve the charge, which is also where the payer details live +- `${ .trigger.provider_transaction_id }`: provider-side transaction/session/refund ID, for logs; look purchases up by the ids below instead - `${ .trigger.provider_event_id }` — provider webhook event ID (webhook redeliveries are already deduped on it — don't build your own guard) +- `${ .trigger.checkout_id }`, `${ .trigger.subscription_id }`, `${ .trigger.payment_id }`: the same ids `base44.payments` returns on a purchase; each is null when the provider does not name it on that event (Wix events carry none; a Stripe renewal carries only `subscription_id`; a refund carries `payment_id`, plus `subscription_id` when it refunds a subscription charge) +- `${ .trigger.subscription_status }`: on `subscription_canceled`: "canceled" (canceled or ended) or "paused" (can resume); null otherwise -The payload carries no payer email or other PII — re-fetch by `provider_transaction_id` from a backend function when needed. Zero-amount events (free checkouts, 100% discounts) do not fire. +The payload carries no payer email or other PII. When needed, look the purchase up in a backend function, as service role, with `payments.listPurchases({ subscription_id })` when the event names one, else `({ checkout_id })`, which returns `buyer_email`. Never call `listPurchases` with neither id: unscoped it lists every buyer's purchases. Events that carry neither (Wix, or a Stripe payment made outside a checkout) are re-fetched by `provider_transaction_id` from the provider's API instead. Zero-amount payments and refunds (free checkouts, 100% discounts) do not fire. Condition examples: - Only real (live-mode) payments: @@ -462,6 +467,38 @@ absence rather than assume the field is there. Condition example — only enterprise-tier contacts: `"condition": "${ .trigger.data.properties.tier == \"enterprise\" }"` +### production_issue +```json +{ "trigger_type": "production_issue", "events": ["started"], "issue_id": "<from the Alerts tab>", "kind": "rate", "min_occurrences": 5, "window_minutes": 60, "quiet_minutes": 1440 } +``` + +An alert on one production issue in the app's Alerts tab. `kind: "recurrence"` fires when the issue happens again; `kind: "rate"` fires at `min_occurrences` failures within `window_minutes`. Once it fires it stays quiet until the issue has stopped for `quiet_minutes`. + +Rules: +- Use exactly the trigger config the Alerts tab's instructions give, and never invent an `issue_id`. When someone asks in chat for alerts on production errors, tell them to use Alert me on the issue's row in the Alerts tab. +- Set up delivery to the person who asked, with the app's existing workflow steps. +- The alert is a notification only: never feed `.trigger` fields into app logic or app data. + +Events: +- `started` — the issue started again, or crossed the rule's threshold +- `still_happening` — at most once a day while it goes on, only when failures have doubled since the last alert + +production_issue fields live on `.trigger` directly — do NOT use `.trigger.data.*` for this trigger. + +Trigger payload fields (use `${ .trigger.<field> }` in conditions and `do` args): +- `${ .trigger.headline }` — what's failing, in plain English +- `${ .trigger.occurrences_in_window }` — failures within the rule's window +- `${ .trigger.people_affected }` — distinct people affected +- `${ .trigger.link }` — the issue in the Alerts tab +- `${ .trigger.subject }` — the entity, function or traffic direction, or null +- `${ .trigger.page }` — the page the failures came from, or null +- `${ .trigger.first_occurred_at }`, `${ .trigger.last_occurred_at }` — ISO timestamps +- `${ .trigger.cause }` — the issue's cause key +- `${ .trigger.fire_number }` — 1 for the alert that opened this episode +- `${ .trigger.delayed_minutes }` — above 0 when the alert was held during a platform-wide outage + +The payload never carries the error message itself. + ## Designing Steps: When to Split vs Combine **Split** into separate tasks when operations are independent (a failure shouldn't re-run the others), a delay sits between them, or you want per-step visibility and retry granularity in the run log. **Combine** into one step when operations share setup/auth/transaction, must be atomic, or the second needs the first's full in-memory result. @@ -595,7 +632,7 @@ write_file("base44/workflows/On First Publish.jsonc", { ``` ### "When someone pays, send a thank-you email" -1. Create backend function: `sendPaymentThankYou` — it looks up the payer's email from the provider API using the transaction ID, then sends the email +1. Create backend function: `sendPaymentThankYou`: as service role it calls `payments.listPurchases({ subscription_id })` when given one, else `({ checkout_id })`, and else, with neither id, re-fetches the payer from the provider's API by `transaction_id`; it emails that `buyer_email`, and sends nothing when no purchase matches 2. Create workflow: ```json write_file("base44/workflows/On Payment.jsonc", { @@ -607,7 +644,24 @@ write_file("base44/workflows/On Payment.jsonc", { }, "definition": { "document": { "dsl": "1.0.0", "name": "on_payment", "version": "1.0", "namespace": "base44" }, - "do": [{ "send_thank_you": { "call": "invoke_backend_function", "with": { "function_name": "sendPaymentThankYou", "args": { "transaction_id": "${ .trigger.provider_transaction_id }", "amount": "${ .trigger.amount }", "currency": "${ .trigger.currency }" } }, "then": "end" } }] + "do": [{ "send_thank_you": { "call": "invoke_backend_function", "with": { "function_name": "sendPaymentThankYou", "args": { "checkout_id": "${ .trigger.checkout_id }", "subscription_id": "${ .trigger.subscription_id }", "transaction_id": "${ .trigger.provider_transaction_id }", "amount": "${ .trigger.amount }", "currency": "${ .trigger.currency }" } }, "then": "end" } }] + } +}) +``` + +### "Alert me when sign-ups fail 5 or more times within an hour" +1. Create backend function: `sendProductionAlert` +2. Create workflow, with the trigger config exactly as the Alerts tab gave it: +```json +write_file("base44/workflows/Alert Sign-ups fail.jsonc", { + "name": "Alert: Sign-ups fail", + "description": "Tells the owner when sign-ups fail 5 or more times within an hour", + "trigger": { + "config": { "trigger_type": "production_issue", "events": ["started"], "issue_id": "6650f0c2a1b2c3d4e5f60718", "kind": "rate", "min_occurrences": 5, "window_minutes": 60, "quiet_minutes": 1440 } + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "alert_signups_fail", "version": "1.0", "namespace": "base44" }, + "do": [{ "send_alert": { "call": "invoke_backend_function", "with": { "function_name": "sendProductionAlert", "args": { "headline": "${ .trigger.headline }", "count": "${ .trigger.occurrences_in_window }", "link": "${ .trigger.link }" } }, "then": "end" } }] } }) ``` diff --git a/skills/base44-platform-workflows/references/web-tanstack.md b/skills/base44-platform-workflows/references/web-tanstack.md index 7caf66b..36000d5 100644 --- a/skills/base44-platform-workflows/references/web-tanstack.md +++ b/skills/base44-platform-workflows/references/web-tanstack.md @@ -21,8 +21,9 @@ What workflows can be built from; the authoring guide that follows documents eac - in_app_agent — Fires when a conversation starts with an in-app agent. - app_user_auth — Fires when an app user signs up or logs in. - app_publish — Fires when the app is published from the builder. -- app_payment — Fires when a payment succeeds or is refunded in the app's payment provider. +- app_payment — Fires when a payment succeeds or is refunded, or a subscription is canceled, in the app's payment provider. - webhook — Fires on an inbound POST to a reusable workspace webhook URL, from any source. Use it for a third-party event no connector trigger covers - don't connect that provider or poll it to get the event. +- production_issue — Fires when a production issue the owner set an alert on happens again or crosses a threshold. ### Activities (what a `call` step can do) - app_security_scan — Security scan (same as the Security tab): `start` kicks it off, `results` reads the latest findings. @@ -391,29 +392,33 @@ Condition examples: { "trigger_type": "app_payment", "events": ["payment_succeeded"] } ``` -Requires a connected payment provider (Stripe, Wix Payments, or Payments by Wix) — activating a workflow with this trigger fails when none is connected. Connect a provider first, then create the workflow. The payload is normalized across providers; workflows never receive provider-shaped raw data. +Requires a connected payment provider (Stripe, PayPal, Wix Payments, or Payments by Wix): activating a workflow with this trigger fails when none is connected. Connect a provider first, then create the workflow. The payload is normalized across providers; workflows never receive provider-shaped raw data. -Valid events: `payment_succeeded` (a successful payment — a completed checkout and the first payment of a subscription), `payment_refunded` (an approved refund). Failed payment attempts do NOT fire this trigger. +Valid events: `payment_succeeded` (a successful payment: a completed checkout and the first payment of a subscription), `payment_refunded` (an approved refund, including one made from the Payments dashboard or the provider's own site), `subscription_canceled` (a subscription stopped billing: canceled or ended on Stripe and PayPal, and paused/suspended on PayPal only, e.g. by the buyer in PayPal or after failed payments; a Stripe pause fires nothing; a PayPal subscription resumed outside the app fires nothing and `listPurchases` can't show it, so tell the user to restore that access by hand). Failed payment attempts do NOT fire this trigger. -Recurring subscription renewal charges are wired to fire `payment_succeeded` too, but delivery is not enabled yet for existing Stripe accounts (it needs a Stripe app permission + per-account re-consent). Do not build a workflow whose only purpose is reacting to renewals, and tell the user renewals may not fire yet if they ask for one. +Use `payment_refunded` and `subscription_canceled` to revoke what a purchase granted: match the event to the purchase the app saved by whichever of `subscription_id`, `checkout_id` and `payment_id` it carries, and make the revoke idempotent. + +Recurring subscription renewal charges are wired to fire `payment_succeeded` too, but delivery is not enabled yet for existing Stripe accounts (it needs a Stripe app permission + per-account re-consent). Do not build a workflow whose only purpose is reacting to renewals, and tell the user renewals may not fire yet if they ask for one. The same holds for `subscription_canceled` on Stripe; on PayPal both fire. When renewals do arrive, the payload does not distinguish them from a first subscription payment — both are `payment_succeeded` with `mode: "subscription"`. So do not use this trigger for one-time-only actions on a subscription (welcome emails, provisioning, granting access) without your own guard, e.g. an entity record keyed by `provider_customer_id` that the workflow checks and writes. app_payment fields live on `.trigger` directly — do NOT use `.trigger.data.*` for this trigger. Trigger payload fields (use `${ .trigger.<field> }` in conditions and `do` args): -- `${ .trigger.event_type }` — "payment_succeeded" or "payment_refunded" -- `${ .trigger.provider }` — "stripe", "wix_payments", or "payments_by_wix" -- `${ .trigger.amount }` — integer amount in cents for most currencies (e.g. 2500 = $25.00), matching the platform's payment analytics representation +- `${ .trigger.event_type }`: "payment_succeeded", "payment_refunded" or "subscription_canceled" +- `${ .trigger.provider }`: "stripe", "paypal", "wix_payments", or "payments_by_wix" +- `${ .trigger.amount }`: integer amount in cents for most currencies (e.g. 2500 = $25.00), matching the platform's payment analytics representation; 0 on `subscription_canceled` - `${ .trigger.currency }` — lowercase ISO currency code (e.g. "usd") - `${ .trigger.is_live }` — false for test/sandbox payments; test payments DO fire this trigger -- `${ .trigger.mode }` — "payment" (one-off checkout) or "subscription" (a subscription charge — first payment OR renewal, indistinguishable); null on refunds. Only Stripe distinguishes these — Wix payments always report "payment" +- `${ .trigger.mode }`: "payment" (one-off checkout) or "subscription" (a subscription charge, first payment OR renewal, indistinguishable); null on refunds. Only Stripe and PayPal distinguish these; Wix payments always report "payment" - `${ .trigger.occurred_at }` — ISO timestamp of the provider event - `${ .trigger.provider_customer_id }` — provider-side customer/contact ID, or null -- `${ .trigger.provider_transaction_id }` — provider-side transaction/session/refund ID; use it to re-fetch full details (payer email, line items) from a server route via the provider's API. On Stripe refunds this is the CHARGE ID (`ch_...`), not a refund ID — retrieve the charge, which is also where the payer details live +- `${ .trigger.provider_transaction_id }`: provider-side transaction/session/refund ID, for logs; look purchases up by the ids below instead - `${ .trigger.provider_event_id }` — provider webhook event ID (webhook redeliveries are already deduped on it — don't build your own guard) +- `${ .trigger.checkout_id }`, `${ .trigger.subscription_id }`, `${ .trigger.payment_id }`: the same ids `base44.payments` returns on a purchase; each is null when the provider does not name it on that event (Wix events carry none; a Stripe renewal carries only `subscription_id`; a refund carries `payment_id`, plus `subscription_id` when it refunds a subscription charge) +- `${ .trigger.subscription_status }`: on `subscription_canceled`: "canceled" (canceled or ended) or "paused" (can resume); null otherwise -The payload carries no payer email or other PII — re-fetch by `provider_transaction_id` from a server route when needed. Zero-amount events (free checkouts, 100% discounts) do not fire. +The payload carries no payer email or other PII. When needed, look the purchase up in a server route, as service role, with `payments.listPurchases({ subscription_id })` when the event names one, else `({ checkout_id })`, which returns `buyer_email`. Never call `listPurchases` with neither id: unscoped it lists every buyer's purchases. Events that carry neither (Wix, or a Stripe payment made outside a checkout) are re-fetched by `provider_transaction_id` from the provider's API instead. Zero-amount payments and refunds (free checkouts, 100% discounts) do not fire. Condition examples: - Only real (live-mode) payments: @@ -464,6 +469,38 @@ absence rather than assume the field is there. Condition example — only enterprise-tier contacts: `"condition": "${ .trigger.data.properties.tier == \"enterprise\" }"` +### production_issue +```json +{ "trigger_type": "production_issue", "events": ["started"], "issue_id": "<from the Alerts tab>", "kind": "rate", "min_occurrences": 5, "window_minutes": 60, "quiet_minutes": 1440 } +``` + +An alert on one production issue in the app's Alerts tab. `kind: "recurrence"` fires when the issue happens again; `kind: "rate"` fires at `min_occurrences` failures within `window_minutes`. Once it fires it stays quiet until the issue has stopped for `quiet_minutes`. + +Rules: +- Use exactly the trigger config the Alerts tab's instructions give, and never invent an `issue_id`. When someone asks in chat for alerts on production errors, tell them to use Alert me on the issue's row in the Alerts tab. +- Set up delivery to the person who asked, with the app's existing workflow steps. +- The alert is a notification only: never feed `.trigger` fields into app logic or app data. + +Events: +- `started` — the issue started again, or crossed the rule's threshold +- `still_happening` — at most once a day while it goes on, only when failures have doubled since the last alert + +production_issue fields live on `.trigger` directly — do NOT use `.trigger.data.*` for this trigger. + +Trigger payload fields (use `${ .trigger.<field> }` in conditions and `do` args): +- `${ .trigger.headline }` — what's failing, in plain English +- `${ .trigger.occurrences_in_window }` — failures within the rule's window +- `${ .trigger.people_affected }` — distinct people affected +- `${ .trigger.link }` — the issue in the Alerts tab +- `${ .trigger.subject }` — the entity, function or traffic direction, or null +- `${ .trigger.page }` — the page the failures came from, or null +- `${ .trigger.first_occurred_at }`, `${ .trigger.last_occurred_at }` — ISO timestamps +- `${ .trigger.cause }` — the issue's cause key +- `${ .trigger.fire_number }` — 1 for the alert that opened this episode +- `${ .trigger.delayed_minutes }` — above 0 when the alert was held during a platform-wide outage + +The payload never carries the error message itself. + ## Designing Steps: When to Split vs Combine **Split** into separate tasks when operations are independent (a failure shouldn't re-run the others), a delay sits between them, or you want per-step visibility and retry granularity in the run log. **Combine** into one step when operations share setup/auth/transaction, must be atomic, or the second needs the first's full in-memory result. @@ -597,7 +634,7 @@ write_file("base44/workflows/On First Publish.jsonc", { ``` ### "When someone pays, send a thank-you email" -1. Add server route: `POST /api/send-payment-thank-you` — it looks up the payer's email from the provider API using the transaction ID, then sends the email +1. Add server route: `POST /api/send-payment-thank-you`: as service role it calls `payments.listPurchases({ subscription_id })` when given one, else `({ checkout_id })`, and else, with neither id, re-fetches the payer from the provider's API by `transaction_id`; it emails that `buyer_email`, and sends nothing when no purchase matches 2. Create workflow: ```json write_file("base44/workflows/On Payment.jsonc", { @@ -609,7 +646,24 @@ write_file("base44/workflows/On Payment.jsonc", { }, "definition": { "document": { "dsl": "1.0.0", "name": "on_payment", "version": "1.0", "namespace": "base44" }, - "do": [{ "send_thank_you": { "call": "invoke_server_route", "with": { "path": "/api/send-payment-thank-you", "body": { "transaction_id": "${ .trigger.provider_transaction_id }", "amount": "${ .trigger.amount }", "currency": "${ .trigger.currency }" } }, "then": "end" } }] + "do": [{ "send_thank_you": { "call": "invoke_server_route", "with": { "path": "/api/send-payment-thank-you", "body": { "checkout_id": "${ .trigger.checkout_id }", "subscription_id": "${ .trigger.subscription_id }", "transaction_id": "${ .trigger.provider_transaction_id }", "amount": "${ .trigger.amount }", "currency": "${ .trigger.currency }" } }, "then": "end" } }] + } +}) +``` + +### "Alert me when sign-ups fail 5 or more times within an hour" +1. Add server route: `POST /api/send-production-alert` +2. Create workflow, with the trigger config exactly as the Alerts tab gave it: +```json +write_file("base44/workflows/Alert Sign-ups fail.jsonc", { + "name": "Alert: Sign-ups fail", + "description": "Tells the owner when sign-ups fail 5 or more times within an hour", + "trigger": { + "config": { "trigger_type": "production_issue", "events": ["started"], "issue_id": "6650f0c2a1b2c3d4e5f60718", "kind": "rate", "min_occurrences": 5, "window_minutes": 60, "quiet_minutes": 1440 } + }, + "definition": { + "document": { "dsl": "1.0.0", "name": "alert_signups_fail", "version": "1.0", "namespace": "base44" }, + "do": [{ "send_alert": { "call": "invoke_server_route", "with": { "path": "/api/send-production-alert", "body": { "headline": "${ .trigger.headline }", "count": "${ .trigger.occurrences_in_window }", "link": "${ .trigger.link }" } }, "then": "end" } }] } }) ``` From 43951ffce96bb2d7607d69aa3776d13ab30d2093 Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Tue, 20 Oct 2026 10:48:57 +0000 Subject: [PATCH 04/15] ci: trust the existing base44-github-actions app for compiled-skill sync PRs The apper sync opens its PRs with the base44-github-actions app, which this repo's CLI and SDK sync workflows already trust, so the auto-merge guard names that bot directly and no SKILLS_SYNC_BOT_LOGIN variable is needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLKGecSgMqR14kYQTrugoR --- .github/workflows/auto-merge-skill-update.yml | 10 +++++----- CONTRIBUTING.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/auto-merge-skill-update.yml b/.github/workflows/auto-merge-skill-update.yml index 4e150c9..136aac3 100644 --- a/.github/workflows/auto-merge-skill-update.yml +++ b/.github/workflows/auto-merge-skill-update.yml @@ -1,13 +1,13 @@ name: Auto-merge compiled skill updates -# A nightly sync PR from base44-dev/apper (label `auto-skill-update`, opened by the sync app) that +# A nightly sync PR from base44-dev/apper (label `auto-skill-update`, opened by the +# base44-github-actions app, the same bot sync-cli-skill.yml / sync-sdk-skill.yml trust) that # touches only the compiled `skills/base44-platform-*` tree and the APPER_COMMIT pin is merged # automatically once `Validate base44-platform skills` passes. Anything else with the label (the # CLI / SDK sync PRs, a hand-made PR) is left for a human. # -# Requires: GitHub auto-merge enabled on this repository, the validator as a required check on -# main, and `vars.SKILLS_SYNC_BOT_LOGIN` set to the sync app's login (for example -# `base44-skills-sync[bot]`). See CONTRIBUTING.md, "Compiled platform skills". +# Requires: GitHub auto-merge enabled on this repository and the validator as a required check +# on main. See CONTRIBUTING.md, "Compiled platform skills". on: pull_request: @@ -19,7 +19,7 @@ permissions: jobs: enable-auto-merge: - if: contains(github.event.pull_request.labels.*.name, 'auto-skill-update') && github.event.pull_request.user.login == vars.SKILLS_SYNC_BOT_LOGIN + if: contains(github.event.pull_request.labels.*.name, 'auto-skill-update') && github.event.pull_request.user.login == 'base44-github-actions[bot]' runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22bffa8..f52fded 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -136,8 +136,8 @@ They describe the platform exactly as the in-editor AI learns it, one reference checks that the PR carries exactly what the compiler emitted: the generated-file header on every file, sha256 hashes matching `skills/base44-platform-manifest.json`, frontmatter whose `name` equals the directory name, resolving links, and a 40-hex `APPER_COMMIT` pin. -3. `Auto-merge compiled skill updates` enables squash auto-merge on a labeled PR opened by the sync - app whose diff is confined to `skills/base44-platform-*/**` and `APPER_COMMIT`. Everything else +3. `Auto-merge compiled skill updates` enables squash auto-merge on a labeled PR opened by + `base44-github-actions[bot]` whose diff is confined to `skills/base44-platform-*/**` and `APPER_COMMIT`. Everything else waits for a human. The Slack notifier posts the PR like any other skill-sync PR. ### How to change a compiled skill @@ -171,9 +171,9 @@ paths to read. ### Operating the pipeline (maintainers) -- The sync PR is opened with a GitHub App installed on this repository (contents and pull requests, - write). Apper reads it from `vars.BASE44_SKILLS_APP_ID` and `secrets.BASE44_SKILLS_APP_PRIVATE_KEY`; - this repo needs `vars.SKILLS_SYNC_BOT_LOGIN` set to the app's login so auto-merge recognizes its PRs. +- The sync PR is opened by the `base44-github-actions` GitHub App (id 2616622), the same app the + CLI and SDK release workflows use to dispatch here. Apper reads it from + `vars.BASE44_GITHUB_ACTIONS_APP_ID` and `secrets.BASE44_GITHUB_ACTIONS_APP_PRIVATE_KEY`. - Auto-merge must be enabled in the repository settings, with `Validate base44-platform skills` as a required check on `main`. - `.github/CODEOWNERS` routes the generated tree and the two workflows to the owning team. From 7d6e82830838977c84721c2faea6befa4a0cdd78 Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Tue, 20 Oct 2026 11:35:02 +0000 Subject: [PATCH 05/15] sync: compile Core integrations as routed through server code (apper 816ecd2112c) Regenerated from apper feat/agent-skills-compiler at 816ecd2112c. New apps are created with core-integration control on, so the integrations-sdk and backend-functions references now teach Core calls inside backend/server functions with asServiceRole, name the agent-profile value core_integrations: backend_functions_only | client_allowed, and the integrations-sdk web reference is split into web-spa and web-tanstack. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2pKgYtPwVgNkRKbWFPUcB --- APPER_COMMIT | 2 +- .../references/mobile.md | 7 +- .../references/web-spa.md | 5 +- .../base44-platform-integrations-sdk/SKILL.md | 6 +- .../references/mobile.md | 35 ++-- .../references/web-spa.md | 186 ++++++++++++++++++ .../references/{web.md => web-tanstack.md} | 23 ++- skills/base44-platform-manifest.json | 11 +- 8 files changed, 240 insertions(+), 35 deletions(-) create mode 100644 skills/base44-platform-integrations-sdk/references/web-spa.md rename skills/base44-platform-integrations-sdk/references/{web.md => web-tanstack.md} (90%) diff --git a/APPER_COMMIT b/APPER_COMMIT index 47573e7..b347f10 100644 --- a/APPER_COMMIT +++ b/APPER_COMMIT @@ -1 +1 @@ -5712fcc95b9241f84ec55318763f7b0ee5d2df2a +816ecd2112cd23154c79b2ee6c24b8486e87d8af diff --git a/skills/base44-platform-backend-functions/references/mobile.md b/skills/base44-platform-backend-functions/references/mobile.md index 84318d4..d371fda 100644 --- a/skills/base44-platform-backend-functions/references/mobile.md +++ b/skills/base44-platform-backend-functions/references/mobile.md @@ -7,6 +7,8 @@ When a user asks you to integrate with an external API that has no built-in inte The backend functions are basically an HTTP handler that you can define in `base44/functions/{functionName}/entry.ts`. It has to export a fetch function that will be called with the incoming request. Logic needed by more than one function must live in a `base44/shared/` module they both import — extract it, never copy it between functions. +The Core-integration routing rule is an exception: use a backend function for Core methods except `UploadPublicFile`, `UploadPrivateFile` and `CreateFileSignedUrl` (`UploadFile` is the retired spelling of `UploadPublicFile`). + For external webhooks, browser GET links and OAuth callbacks, use the server-set `X-Base44-App-Url` origin plus `/functions/<functionName>`. Never expose dispatcher `/run/<scriptId>` or deployment URLs; the dispatcher requires platform-only authentication. Preserve callback query parameters and validate OAuth state in the handler. <code_validation> @@ -28,6 +30,7 @@ For external webhooks, browser GET links and OAuth callbacks, use the server-set - must return Response objects - never return strings or plain objects directly - must include proper try/catch error handling for robustness - must validate user authentication with `base44.auth.me()` when using Base44 SDK +- Exception: use `base44.asServiceRole.integrations.Core` for Core-integration functions. Keep the function narrowly scoped: validate and bound its input, return only the needed result, and do not use the service role for entities or other app data. - must use proper JavaScript/TypeScript syntax with matched brackets and braces - must validate all regex patterns are syntactically correct before writing - must not throw or `await` at module top-level — keep all init inside the handler (top-level failures cause boot errors, not 500s) @@ -138,8 +141,8 @@ export default async function(req) { </example> 16.c When to use service role vs user auth: * Prefer user-scoped calls (`base44.entities/...`) when responding to frontend/user actions. - * Use service role (`base44.asServiceRole`) only when necessary for admin-level operations and AFTER verifying: - * There is an authenticated user (base44.auth.isAuthenticated() is true), and + * Use service role (`base44.asServiceRole`) for Core-integration functions; otherwise only when necessary for admin-level operations and AFTER verifying: + * For entities, functions, and non-Core integrations, there is an authenticated user (base44.auth.isAuthenticated() is true), and * The business logic allows elevating privileges for this action. * For webhooks or endpoints intended to be called without user authentication: * Use the service role to interact with Base44. diff --git a/skills/base44-platform-backend-functions/references/web-spa.md b/skills/base44-platform-backend-functions/references/web-spa.md index 06fe1fa..920466d 100644 --- a/skills/base44-platform-backend-functions/references/web-spa.md +++ b/skills/base44-platform-backend-functions/references/web-spa.md @@ -7,6 +7,8 @@ Backend functions: HTTP handlers in `base44/functions/{functionName}/entry.ts`, for external APIs with no built-in integration. Always prefer built-in integrations when they cover the need. Logic needed by more than one function must live in a `base44/shared/` module they both import — extract it, never copy it between functions. </overview> +The Core-integration routing rule is an exception: use a backend function for Core methods except `UploadPublicFile`, `UploadPrivateFile` and `CreateFileSignedUrl` (`UploadFile` is the retired spelling of `UploadPublicFile`). + <integration_priority> For external-service integrations, in order: 1. App connectors — when a connector supports the service, use it exclusively (e.g. Google Calendar via its connector). @@ -33,6 +35,7 @@ Requirements: - try/catch the handler body; return a proper error response. - `await` every Base44 SDK call — `base44.entities.X.list()`, `base44.auth.me()`, `base44.functions.invoke(...)` all return Promises. - Validate user auth with `base44.auth.me()` when using the SDK. +- Exception: use `base44.asServiceRole.integrations.Core` for Core-integration functions. Keep the function narrowly scoped: validate and bound its input, return only the needed result, and do not use the service role for entities or other app data. - Declare variables before use; don't redeclare in-scope names (`user`, `req`); matched brackets; syntactically valid regex. - Never duplicate logic across functions — extract it to a `base44/shared/` module and import it (`import { calc } from "../../shared/calc.ts"`); own-dir helpers import as `./util.ts`. - `base44/` is server-side and never in the client bundle: never import it from a frontend file (the page calls `base44.functions.invoke(...)` instead), and never import `src/` or `@/` from a function. @@ -79,7 +82,7 @@ export default async function(req) { } ``` -Prefer user-scoped calls (`base44.entities...`) for frontend/user actions. `base44.asServiceRole` (exposes `entities`, `functions`, `integrations`) only when admin-level operations are necessary, after verifying `base44.auth.isAuthenticated()` is true and the business logic allows the elevation. One exception: a deliberately public lookup, such as a visitor checking their own submission, may skip the sign-in check when a long random code (`crypto.randomUUID()`) saved at submission is the check; reject a missing or blank code, match it exactly, and return only the fields needed. +Prefer user-scoped calls (`base44.entities...`) for frontend/user actions. Use `base44.asServiceRole.integrations.Core` for Core-integration functions. For entities, functions, and non-Core integrations, use `base44.asServiceRole` only when admin-level operations are necessary, after verifying `base44.auth.isAuthenticated()` is true and the business logic allows the elevation. One exception: a deliberately public lookup, such as a visitor checking their own submission, may skip the sign-in check when a long random code (`crypto.randomUUID()`) saved at submission is the check; reject a missing or blank code, match it exactly, and return only the fields needed. </base44_sdk_usage> <webhooks> diff --git a/skills/base44-platform-integrations-sdk/SKILL.md b/skills/base44-platform-integrations-sdk/SKILL.md index 2703c8f..dcb38ee 100644 --- a/skills/base44-platform-integrations-sdk/SKILL.md +++ b/skills/base44-platform-integrations-sdk/SKILL.md @@ -7,7 +7,7 @@ description: "Base44 built-in integrations on the SDK client: InvokeLLM, SendEma Edits here are overwritten by the nightly sync; change the source partials in apper. --> # Base44 integrations sdk -Core integrations are callable from the browser SDK and from server code; the endpoint schemas below are the ones the platform serves today. +File uploads and signed URLs are called from the browser SDK; every other Core integration runs in server code on an app with core-integration control, the default for new apps (profile `core_integrations: backend_functions_only`). The endpoint schemas below are the ones the platform serves today. ## Pick the reference for this app @@ -19,6 +19,6 @@ native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwi | Profile | Read | |---|---| -| Web app, Vite SPA (the default web template) | [Integrations and analytics SDK (web apps)](references/web.md) | -| Web app, TanStack Start | [Integrations and analytics SDK (web apps)](references/web.md) | +| Web app, Vite SPA (the default web template) | [Integrations and analytics SDK (Vite SPA)](references/web-spa.md) | +| Web app, TanStack Start | [Integrations and analytics SDK (TanStack Start)](references/web-tanstack.md) | | Native mobile app (Expo, React Native) | [Integrations SDK (mobile apps)](references/mobile.md) | diff --git a/skills/base44-platform-integrations-sdk/references/mobile.md b/skills/base44-platform-integrations-sdk/references/mobile.md index a269d50..1d66571 100644 --- a/skills/base44-platform-integrations-sdk/references/mobile.md +++ b/skills/base44-platform-integrations-sdk/references/mobile.md @@ -8,19 +8,26 @@ Integrations are packages of functions that are used to interact with external d You can use integration packages from the pre-initialized base44 sdk client. and call spefici integrations by with the relevant parameters. -for example: +<core_integrations_backend_functions> +All `base44.integrations.Core` SDK methods except `UploadPublicFile`, `UploadPrivateFile` and `CreateFileSignedUrl` must run inside a backend function using `base44.asServiceRole.integrations.Core`. Check every Core Integration call in the client code. The restricted methods cannot remain there. `UploadFile` is the retired spelling of `UploadPublicFile` and is equally client-callable. + +Use a clearly named, narrow app operation, not a 1:1 Core proxy: hardcode the Core method, expose and validate only the bounded inputs the feature needs, keep privileged configuration server-side, and return only the required result. Never accept an integration name plus an arbitrary payload for forwarding. +Limit each request's cost and scope (for example prompt/file size, item count, output count, or fixed recipients/destinations) and add feature-appropriate abuse controls when available. + +Upload files directly from the client with `UploadPublicFile` or `UploadPrivateFile`. Pass the resulting `file_url` or `file_uri` to a backend function for follow-on Core work such as transcription or extraction. Signing is not such work: call `CreateFileSignedUrl` from the client with the `file_uri`, rather than brokering it through a function of your own. This is the shipped default: the app profile reports `core_integrations: backend_functions_only`. Only on an app whose profile reports `client_allowed` may the client call the restricted Core methods directly. + +The `base44-platform-backend-functions` skill (mobile reference) covers file placement, the frontend invocation path and testing; use the Core service-role rule above for the function body. +</core_integrations_backend_functions> + +```js import { base44 } from "@/api/base44Client"; -const res = await base44.integrations.Core.InvokeLLM({ - prompt: "Give me data on Apple (the company)", - add_context_from_internet: true, - response_json_schema: { - type: "object", - properties: { - stock_price: {type: "number"}, - news_headlines: {type: "array", items: {type: "string"}}, - }, - } -}); +// private end-user uploads: +const {file_uri} = await base44.integrations.Core.UploadPrivateFile({file}); +const {signed_url} = await base44.integrations.Core.CreateFileSignedUrl({file_uri}); + +// public end-user uploads: +const {file_url} = await base44.integrations.Core.UploadPublicFile({file}); +``` InvokeLLM supports an optional `model` parameter for higher quality output on complex tasks (e.g. model: "claude-sonnet-5"). Non-default models use more integration credits, so only use them when the task requires it. When you use a non-default model, tell the user that it uses more integration credits. Only 'gemini_3_flash' and 'gemini_3_1_pro' support add_context_from_internet. Using an incompatible model will raise an error. @@ -28,10 +35,6 @@ InvokeLLM supports an optional `model` parameter for higher quality output on co Default every end-user upload to `UploadPrivateFile`. Use `UploadPublicFile` only when the user asked for these files to be public, or when something outside the app must fetch the file later without signing in. </end_user_uploads> -another example: -import { base44 } from "@/api/base44Client"; -const {file_uri} = await base44.integrations.Core.UploadPrivateFile({file: file}); - `SendEmail` reaches registered app users normally. This app may ALSO email addresses that are not registered users, but only with a connected custom domain, on a paid plan, and subject to content screening, recipient-hygiene and anti-abuse limits that can refuse or pause sends — so never promise delivery to a non-registered address, and never route untrusted end-user input straight into the recipient. Invitations still use `base44.users.inviteUser`. Here are the integration packages available in this app: diff --git a/skills/base44-platform-integrations-sdk/references/web-spa.md b/skills/base44-platform-integrations-sdk/references/web-spa.md new file mode 100644 index 0000000..c20e783 --- /dev/null +++ b/skills/base44-platform-integrations-sdk/references/web-spa.md @@ -0,0 +1,186 @@ +<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the + builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. + Edits here are overwritten by the nightly sync; change the source partials in apper. --> +# Base44 integrations sdk: Integrations and analytics SDK (Vite SPA) + +Integrations: function packages for external data/services, on the pre-initialized base44 client: + +<core_integrations_backend_functions> +All `base44.integrations.Core` SDK methods except `UploadPublicFile`, `UploadPrivateFile` and `CreateFileSignedUrl` must run inside a backend function using `base44.asServiceRole.integrations.Core`. Check every Core Integration call in the client code. The restricted methods cannot remain there. `UploadFile` is the retired spelling of `UploadPublicFile` and is equally client-callable. + +Use a clearly named, narrow app operation, not a 1:1 Core proxy: hardcode the Core method, expose and validate only the bounded inputs the feature needs, keep privileged configuration server-side, and return only the required result. Never accept an integration name plus an arbitrary payload for forwarding. +Limit each request's cost and scope (for example prompt/file size, item count, output count, or fixed recipients/destinations) and add feature-appropriate abuse controls when available. + +Upload files directly from the client with `UploadPublicFile` or `UploadPrivateFile`. Pass the resulting `file_url` or `file_uri` to a backend function for follow-on Core work such as transcription or extraction. Signing is not such work: call `CreateFileSignedUrl` from the client with the `file_uri`, rather than brokering it through a function of your own. This is the shipped default: the app profile reports `core_integrations: backend_functions_only`. Only on an app whose profile reports `client_allowed` may the client call the restricted Core methods directly. + +The `base44-platform-backend-functions` skill covers file placement, the frontend invocation path and testing; use the Core service-role rule above for the function body. +</core_integrations_backend_functions> + +```js +// private end-user uploads: +const {file_uri} = await base44.integrations.Core.UploadPrivateFile({file}); +const {signed_url} = await base44.integrations.Core.CreateFileSignedUrl({file_uri}); + +// public end-user uploads: +const {file_url} = await base44.integrations.Core.UploadPublicFile({file}); +``` + +<end_user_uploads> +Default every end-user upload to `UploadPrivateFile`. Use `UploadPublicFile` only when the user asked for these files to be public, or when something outside the app must fetch the file later without signing in. +</end_user_uploads> +InvokeLLM optional `model` (e.g. model: "claude-sonnet-5") for complex tasks — non-default models use more integration credits: only when needed, and tell the user. add_context_from_internet only with 'gemini_3_flash'/'gemini_3_1_pro'; others raise an error. InvokeLLM is a single call with no tools — don't chain it to simulate an agent loop; agents are covered in the agents guide. + +`SendEmail` reaches registered app users normally. This app may ALSO email addresses that are not registered users, but only with a connected custom domain, on a paid plan, and subject to content screening, recipient-hygiene and anti-abuse limits that can refuse or pause sends — so never promise delivery to a non-registered address, and never route untrusted end-user input straight into the recipient. Invitations still use `base44.users.inviteUser`. + +Integration packages in this app: + +<integration_package> +<package_name>Core</package_name> +<package_description>Built-in integrations that are always available.</package_description> + +<endpoint> +<name>InvokeLLM</name> +<description> + Generates a response from an LLM with a prompt. + Be elaborate and detailed in your prompt. + You can potentially ask the llm to output a json, in which case specify the json schema in response_json_schema. + If you need to use external knowledge, use add_context_from_internet=true. This will give the LLM context from google search, google maps, news, etc. That way you can fetch structured data from the web. + You can attach files (including images) for additional context to the LLM, using the `file_urls` parameter. This should be a list of urls after uploading them. + You can use both file_urls AND add_context_from_internet together - the system will use a model that supports both vision and web search. + You can optionally specify a `model` for this call. Options: 'automatic', 'gemini_3_8_flash', 'gpt_6_luna', 'claude-sonnet-5', 'claude_opus_5_5', 'gpt_6_sol', 'gpt_6_astra', 'claude_fable_5_1', 'glm_5_2'. Defaults to automatic. + 'gemini_3_8_flash' and 'gpt_6_luna' are fast; 'claude-sonnet-5' is balanced; 'claude_opus_5_5' and 'gpt_6_sol' are frontier models for hard reasoning; 'claude_fable_5_1' and 'gpt_6_astra' are the strongest and by far the most expensive. + All models except 'glm_5_2' support file_urls. Only 'gemini_3_8_flash' supports add_context_from_internet (web search). Using an incompatible model will raise an error. + Non-default models cost more integration credits. Only use them when the task requires higher quality. + If response_json_schema is specified, returns a dict (so no need to parse it), otherwise returns a string. + IMPORTANT: If specified, response_json_schema root object must by of type object, not array. i.e. {"type": "object", ...} + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'prompt': {'title': 'Prompt', 'type': 'string'}, 'add_context_from_internet': {'default': False, 'title': 'Add Context From Internet', 'type': 'boolean'}, 'response_json_schema': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'type': 'null'}], 'default': None, 'title': 'Response Json Schema'}, 'file_urls': {'anyOf': [{'format': 'binary', 'type': 'string'}, {'items': {'type': 'string'}, 'type': 'array'}, {'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'File Urls'}, 'model': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Model'}}, 'required': ['prompt'], 'title': 'InvokeLLMModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>TranscribeAudio</name> +<description> + Transcribes an audio file to text using speech recognition (Whisper). + First upload the audio file using the UploadPublicFile integration, then pass the resulting URL here. + Returns the transcript as a plain string. + Supported formats: ogg, oga, mp3, wav, webm, m4a, mp4, mpeg, mpga, flac. Maximum file size: 25MB. + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'audio_url': {'title': 'Audio Url', 'type': 'string'}}, 'required': ['audio_url'], 'title': 'TranscribeAudioModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>SendEmail</name> +<description>Sends email. Reaches registered app users always; reaching an address that is not a registered user requires a paid plan and a custom domain connected to the app. Optionally specify the sender name; otherwise the app name is used.</description> +<input_schema>{'$defs': {'EmailAttachment': {'description': 'One attachment as a caller supplies it: a name plus exactly one source.\n\nNo caller-supplied content type: the extension is already constrained to the\nallowlist, so it determines the type and the two can never disagree.', 'properties': {'filename': {'description': 'Name the recipient sees, with an extension from the supported set: csv, docx, gif, ics, jpeg, jpg, md, pdf, png, txt, webp, xlsx', 'title': 'Filename', 'type': 'string'}, 'file_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "URL or storage URI of a file THIS app already uploaded (e.g. the file_url returned by UploadFile). Another app's files are refused.", 'title': 'File Url'}, 'content': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Base64-encoded bytes, for a document the function generated itself. Supply either this or file_url, never both.', 'title': 'Content'}}, 'required': ['filename'], 'title': 'EmailAttachment', 'type': 'object'}}, 'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'from_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'From Name'}, 'to': {'title': 'To', 'type': 'string'}, 'subject': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Subject line. Optional with a template: it overrides the template's own <title>.", 'title': 'Subject'}, 'body': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Body'}, 'html': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "HTML body — the same thing as 'body' under an explicit name. Set one or the other, never both.", 'title': 'Html'}, 'text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Plain-text body. On its own, sends a plain-text email. Alongside 'body'/'html', both go out as one multipart/alternative email and the recipient's mail client renders whichever it prefers — so the two must say the same thing. Not accepted with a template: templates are HTML-only.", 'title': 'Text'}, 'template_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Id'}, 'template_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Name'}, 'variables': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'type': 'null'}], 'default': None, 'title': 'Variables'}, 'attachments': {'anyOf': [{'items': {'$ref': '#/$defs/EmailAttachment'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Attachments'}}, 'required': ['to'], 'title': 'SendEmailModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>SendPushNotification</name> +<description>Sends a native mobile push notification to one app user's phone. + + Use ONLY when the user explicitly asks for mobile push notifications and the + app ships as a native iOS/Android app. This does not reach the web app, and + it is not email or an in-app notification — for those use SendEmail or build + in-app UI instead. Never reach for it to "notify a user" in general. + + The app must have a native mobile build with push credentials configured; + without that the call fails at send time, so do not build a feature around it + unless the user confirmed they have a native mobile app. + + Server-side only: call it from a backend function via `asServiceRole` (a + logged-in end user must not be able to push to other users). + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'user_id': {'title': 'User Id', 'type': 'string'}, 'title': {'title': 'Title', 'type': 'string'}, 'content': {'title': 'Content', 'type': 'string'}, 'action_label': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Action Label'}, 'action_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Action Url'}}, 'required': ['user_id', 'title', 'content'], 'title': 'SendPushNotificationModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>UploadPublicFile</name> +<description> + Uploads a file to permanent, world-readable public storage. + Returns a dict containing {"file_url": string} + Anyone holding the url can fetch the file without signing in. + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'file': {'format': 'binary', 'title': 'File', 'type': 'string'}}, 'required': ['file'], 'title': 'UploadPublicFileModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>GenerateImage</name> +<description>Generate an image using AI. + Takes about 5-10 seconds to generate an image. + Be detailed in the prompt, the better the prompt, the better the image. + Optionally provide existing_image_urls as reference - for editing, style inspiration, or visual context. + The integration returns a URL to the generated image: {url: str}</description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'prompt': {'title': 'Prompt', 'type': 'string'}, 'existing_image_urls': {'anyOf': [{'items': {'type': 'string'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Existing Image Urls'}}, 'required': ['prompt'], 'title': 'GenerateImageModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>GenerateSpeech</name> +<description>Generate stored, shareable speech audio (TTS) from text. Returns an MP3 URL: {url: str}. + Multiple voices, consistent across users, high-quality multilingual (auto-detected if language_code is omitted). + For simple in-page playback, prefer the browser's TTS API. + Cost: 1 integration credit per 50 characters of text, up to 100 per call.</description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'text': {'description': 'Text to convert. Max 5000 chars.', 'title': 'Text', 'type': 'string'}, 'voice': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Optional voice (default 'river'):\n- river: calm, neutral.\n- honey: warm, soft.\n- sunny: bright, upbeat.\n- storm: formal, authoritative.\n- spark: energetic, quick.", 'title': 'Voice'}, 'language_code': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Optional ISO-639-1 code (e.g. 'en', 'es'). Omit to auto-detect.", 'title': 'Language Code'}}, 'required': ['text'], 'title': 'GenerateSpeechModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>GenerateVideo</name> +<description>Generate a video using AI. + + Takes about 30-60 seconds to generate a video using Google's Veo 3.x models. + Be detailed in the prompt - the better the prompt, the better the video. + + The integration returns a URL to the generated video: {url: str} + + Credit Cost: 5 integration credits per second of video + - 4 seconds = 20 credits + - 6 seconds (default) = 30 credits + - 8 seconds = 40 credits + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'prompt': {'title': 'Prompt', 'type': 'string'}, 'duration': {'default': 6, 'enum': [4, 6, 8], 'title': 'Duration', 'type': 'integer'}, 'aspect_ratio': {'default': '16:9', 'enum': ['16:9', '9:16'], 'title': 'Aspect Ratio', 'type': 'string'}, 'generate_audio': {'default': True, 'title': 'Generate Audio', 'type': 'boolean'}}, 'required': ['prompt'], 'title': 'GenerateVideoModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>ExtractDataFromUploadedFile</name> +<description> + Extracts data from an uploaded file. + Use the UploadPublicFile integration to get the file url first. + Supports csv, xlsx, json, html, png, jpg, jpeg, and pdf files. + Useful to extract data, and potential flow - upload file, extract data, bulk insert entities into database. + Only use this integration when you know the target JSON schema of the data you're trying to extract (if you just want the content of a file - fetch the file url). + returns the following object: + { + "status": "success" | "error", + "details": "error details if error, otherwise null", + "output": list[dict] | dict | None (exists if status is success) + } + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'file_url': {'title': 'File Url', 'type': 'string'}, 'json_schema': {'additionalProperties': True, 'title': 'Json Schema', 'type': 'object'}}, 'required': ['file_url', 'json_schema'], 'title': 'ExtractDataFromUploadedFileModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>CreateFileSignedUrl</name> +<description> + Creates a signed URL for a private file that can be used to download the file. + The file must be uploaded to the private storage. + Returns a dict containing {"signed_url": string} + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'file_uri': {'title': 'File Uri', 'type': 'string'}, 'expires_in': {'default': 300, 'title': 'Expires In', 'type': 'integer'}}, 'required': ['file_uri'], 'title': 'CreateFileSignedUrlModel', 'type': 'object'}</input_schema> +</endpoint> + +<endpoint> +<name>UploadPrivateFile</name> +<description> + Uploads a file to the app's private filhes directory. + Returns a dict containing {"file_uri": string} + You can use the file_uri later to create a signed URL for the file using the CreateFileSignedUrl integration + The file is uploaded to the private storage. + </description> +<input_schema>{'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'file': {'format': 'binary', 'title': 'File', 'type': 'string'}}, 'required': ['file'], 'title': 'UploadPrivateFileModel', 'type': 'object'}</input_schema> +</endpoint> + +</integration_package> + +Track custom events with base44 from '@/api/base44Client': +base44.analytics.track({ eventName: string, properties?: Record<string, string | number | boolean | null> }) +Indicative event names (e.g. "user_contact_form_submit"), minimal properties, no PII. +To opt-out, set `{"analytics":{"enabled":false}}` when initializing the base44 SDK (supported from 0.8.46). diff --git a/skills/base44-platform-integrations-sdk/references/web.md b/skills/base44-platform-integrations-sdk/references/web-tanstack.md similarity index 90% rename from skills/base44-platform-integrations-sdk/references/web.md rename to skills/base44-platform-integrations-sdk/references/web-tanstack.md index fadb4c2..8906f43 100644 --- a/skills/base44-platform-integrations-sdk/references/web.md +++ b/skills/base44-platform-integrations-sdk/references/web-tanstack.md @@ -1,17 +1,26 @@ <!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. Edits here are overwritten by the nightly sync; change the source partials in apper. --> -# Base44 integrations sdk: Integrations and analytics SDK (web apps) +# Base44 integrations sdk: Integrations and analytics SDK (TanStack Start) Integrations: function packages for external data/services, on the pre-initialized base44 client: +<core_integrations_backend_functions> +All `base44.integrations.Core` SDK methods except `UploadPublicFile`, `UploadPrivateFile` and `CreateFileSignedUrl` must run inside a server function using `context.getBase44().asServiceRole.integrations.Core`. Check every Core Integration call in the client code. The restricted methods cannot remain there. `UploadFile` is the retired spelling of `UploadPublicFile` and is equally client-callable. + +Use a clearly named, narrow app operation, not a 1:1 Core proxy: hardcode the Core method, expose and validate only the bounded inputs the feature needs, keep privileged configuration server-side, and return only the required result. Never accept an integration name plus an arbitrary payload for forwarding. +Limit each request's cost and scope (for example prompt/file size, item count, output count, or fixed recipients/destinations) and add feature-appropriate abuse controls when available. + +Upload files directly from the client with `UploadPublicFile` or `UploadPrivateFile`. Pass the resulting `file_url` or `file_uri` to a server function for follow-on Core work such as transcription or extraction. Signing is not such work: call `CreateFileSignedUrl` from the client with the `file_uri`, rather than brokering it through a function of your own. This is the shipped default: the app profile reports `core_integrations: backend_functions_only`. Only on an app whose profile reports `client_allowed` may the client call the restricted Core methods directly. +</core_integrations_backend_functions> + ```js -const res = await base44.integrations.Core.InvokeLLM({ - prompt: "Give me data on Apple (the company)", - add_context_from_internet: true, - response_json_schema: {type: "object", properties: {stock_price: {type: "number"}}} -}); -const {file_uri} = await base44.integrations.Core.UploadPrivateFile({file: file}); +// private end-user uploads: +const {file_uri} = await base44.integrations.Core.UploadPrivateFile({file}); +const {signed_url} = await base44.integrations.Core.CreateFileSignedUrl({file_uri}); + +// public end-user uploads: +const {file_url} = await base44.integrations.Core.UploadPublicFile({file}); ``` <end_user_uploads> diff --git a/skills/base44-platform-manifest.json b/skills/base44-platform-manifest.json index 022435e..4cc85d4 100644 --- a/skills/base44-platform-manifest.json +++ b/skills/base44-platform-manifest.json @@ -7,8 +7,8 @@ "skills/base44-platform-agents/references/web-spa.md": "b569d56379dca260f7ab8e0ca767330031ea329e42a2f395c3c5269db372d7ec", "skills/base44-platform-agents/references/web-tanstack.md": "ce8257bdc0b5c316734f143115f3a7dda8ba8358a61a73c00b4ebd1bc72481a9", "skills/base44-platform-backend-functions/SKILL.md": "66e96aec70ce4303462465a0baf9f46342b7e60c70267b1c4b23c38b39e31fc6", - "skills/base44-platform-backend-functions/references/mobile.md": "a98e72be7fdc1c1b511b594648ae1937e5bfb46ff72350fb6c6cd044b2304556", - "skills/base44-platform-backend-functions/references/web-spa.md": "b62f5ffd2ed52fb1b8d5dfedb2e70a2138d3ce30b0aee1023d433df1c6fe1c76", + "skills/base44-platform-backend-functions/references/mobile.md": "c9adfacd9917d2e72e88066a9f1b266e961a023a5a3e977e3e05e7641cd01253", + "skills/base44-platform-backend-functions/references/web-spa.md": "a24eb530b64b2a868ac168d46bb117711282448ce4b393d4783ee76b55dc798e", "skills/base44-platform-backend-functions/references/web-tanstack.md": "0b7f4fb263e82a7c22546cadbf54bd959fa88da9fa4da79bacd467bd037f52b9", "skills/base44-platform-connectors/SKILL.md": "4bb82de0cabd141953434d553a1f1c57754ea07163651ec383ea5f0edb825046", "skills/base44-platform-connectors/references/mobile.md": "a8640d9156acbe12bc89599da1d91f2985fabdd2970c6224c557dfb2219e4b9c", @@ -65,9 +65,10 @@ "skills/base44-platform-games/references/threejs/text_hud_ui.md": "4e3f879b4f4f06eb035a2d9cc8d709e66d2178114018c5107e55c883a53a9de5", "skills/base44-platform-games/references/threejs/triggers_and_collisions.md": "09d686d06ba9a85310250b61113e7c57883a184ae14458899f0458f39cb80be5", "skills/base44-platform-games/references/threejs/vfx_and_juice.md": "c69797aa5d45def521138729eb08ab8b369c0517371b29abe49637381a39729c", - "skills/base44-platform-integrations-sdk/SKILL.md": "b126e9ac424dea008702e2cd54925cfeb76cdbbe9fda31f3ac2c3c043d7c9f92", - "skills/base44-platform-integrations-sdk/references/mobile.md": "57575c8c7ff975add232955b384cb106d81e302646929c7a652176d49723c82c", - "skills/base44-platform-integrations-sdk/references/web.md": "0e38e2cfee66514099b67e201af86284b1ed852bc145df67bce7bb15000048f9", + "skills/base44-platform-integrations-sdk/SKILL.md": "ae75594a282ec160d5ef2b3e691e0528f26191f4daf80469d69a22d8d0ba6c8f", + "skills/base44-platform-integrations-sdk/references/mobile.md": "65e1be881f2a49c6eb1392f0d91bff4e1e4f531bb5bfbd641f474a4d7a495e89", + "skills/base44-platform-integrations-sdk/references/web-spa.md": "ee3729d343a9f0ae8ab7dbd0ffb45aaf29ae2a48ea8a71851e016b500dc9bc6b", + "skills/base44-platform-integrations-sdk/references/web-tanstack.md": "3f9fd4c6dd8be2c8272cbafcadbf888fdde4c7eaebbe1d47437b296636415bef", "skills/base44-platform-platform/SKILL.md": "244f32d1afeb6a730f1bede1d266d59178cf746c98f5f8d25cb71d4aefa866a7", "skills/base44-platform-platform/references/game.md": "2ad9fe16f5286582db4ca2c9a201658d4970fae327db4240ae5ad2316ed043bc", "skills/base44-platform-platform/references/mobile.md": "e6a2cc30e2ea4b355242ddcf21316168df8e71d57c26ee8124d1d08d51c815ad", From fc21678b3842e04633de165d6c8bc5522b87e623 Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Thu, 22 Oct 2026 10:33:32 +0000 Subject: [PATCH 06/15] sync: IP gate, manual cadence, SOURCE_COMMIT pin The compiled tree now comes from a manually dispatched sync behind an IP gate (internal-name scan plus a review of the publishable tree), so the docs, validator, workflows and CODEOWNERS drop the nightly cadence and no longer name the private source repository. The pin file is SOURCE_COMMIT (a bare commit sha); the generated header matches. Tree recompiled from 7acabe324ef. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLKGecSgMqR14kYQTrugoR --- .github/CODEOWNERS | 6 +- .github/workflows/auto-merge-skill-update.yml | 6 +- .github/workflows/notify-slack-skill-sync.yml | 4 +- .../workflows/validate-base44-platform.yml | 6 +- APPER_COMMIT | 1 - CONTRIBUTING.md | 55 +++--- README.md | 22 +-- SOURCE_COMMIT | 1 + scripts/validate-base44-platform.mjs | 8 +- skills/base44-platform-actors/SKILL.md | 6 +- .../references/mobile.md | 6 +- .../base44-platform-actors/references/web.md | 6 +- skills/base44-platform-agents/SKILL.md | 6 +- .../references/web-spa.md | 8 +- .../references/web-tanstack.md | 8 +- .../SKILL.md | 6 +- .../references/mobile.md | 6 +- .../references/web-spa.md | 6 +- .../references/web-tanstack.md | 6 +- skills/base44-platform-connectors/SKILL.md | 6 +- .../references/mobile.md | 6 +- .../references/web-spa.md | 6 +- .../references/web-tanstack.md | 6 +- skills/base44-platform-entities/SKILL.md | 6 +- .../references/mobile-and-games.md | 6 +- .../references/rls.md | 6 +- .../references/web.md | 6 +- skills/base44-platform-frontend/SKILL.md | 6 +- .../references/mobile.md | 6 +- .../references/web-spa.md | 6 +- .../references/web-tanstack.md | 6 +- skills/base44-platform-games/SKILL.md | 6 +- .../references/game-2d.md | 6 +- .../references/game-3d.md | 6 +- .../phaser/actions_and_utilities.md | 6 +- .../references/phaser/animations.md | 6 +- .../references/phaser/audio_and_sound.md | 6 +- .../references/phaser/cameras.md | 6 +- .../references/phaser/curves_and_paths.md | 6 +- .../references/phaser/data_manager.md | 6 +- .../references/phaser/events_system.md | 6 +- .../references/phaser/filters_and_postfx.md | 6 +- .../phaser/game_object_components.md | 6 +- .../phaser/game_setup_and_config.md | 6 +- .../references/phaser/geometry_and_math.md | 6 +- .../references/phaser/graphics_and_shapes.md | 6 +- .../phaser/groups_and_containers.md | 6 +- .../phaser/input_keyboard_mouse_touch.md | 6 +- .../references/phaser/loading_assets.md | 6 +- .../references/phaser/mesh_and_3d_elements.md | 6 +- .../references/phaser/particles.md | 6 +- .../references/phaser/physics_arcade.md | 6 +- .../references/phaser/physics_matter.md | 6 +- .../references/phaser/render_textures.md | 6 +- .../references/phaser/scale_and_responsive.md | 6 +- .../references/phaser/scenes.md | 6 +- .../references/phaser/sprites_and_images.md | 6 +- .../references/phaser/text_and_bitmaptext.md | 6 +- .../references/phaser/tilemaps.md | 6 +- .../references/phaser/time_and_timers.md | 6 +- .../references/phaser/tweens.md | 6 +- .../threejs/assets_models_textures.md | 6 +- .../references/threejs/cameras.md | 6 +- .../threejs/character_controller.md | 6 +- .../threejs/enemies_and_movement.md | 6 +- .../references/threejs/game_state_and_flow.md | 6 +- .../references/threejs/input_and_controls.md | 6 +- .../references/threejs/performance.md | 6 +- .../references/threejs/physics_dynamics.md | 6 +- .../threejs/projectiles_and_pooling.md | 6 +- .../references/threejs/scene_and_rendering.md | 6 +- .../references/threejs/text_hud_ui.md | 6 +- .../threejs/triggers_and_collisions.md | 6 +- .../references/threejs/vfx_and_juice.md | 6 +- .../base44-platform-integrations-sdk/SKILL.md | 6 +- .../references/mobile.md | 6 +- .../references/web-spa.md | 6 +- .../references/web-tanstack.md | 6 +- skills/base44-platform-manifest.json | 158 +++++++++--------- skills/base44-platform-platform/SKILL.md | 6 +- .../references/game.md | 6 +- .../references/mobile.md | 6 +- .../references/web-spa.md | 6 +- .../references/web-tanstack.md | 6 +- skills/base44-platform-workflows/SKILL.md | 6 +- .../references/automations-legacy.md | 6 +- .../references/mobile-automations.md | 6 +- .../references/web-spa.md | 6 +- .../references/web-tanstack.md | 6 +- 89 files changed, 367 insertions(+), 378 deletions(-) delete mode 100644 APPER_COMMIT create mode 100644 SOURCE_COMMIT diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ce1623e..6d3cd1b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,9 +1,9 @@ # The compiled platform skills are generated from the Base44 builder's prompt tree in -# base44-dev/apper and synced here nightly (CONTRIBUTING.md, "Compiled platform skills"). +# the Base44 builder prompt and synced here by Base44 (CONTRIBUTING.md, "Compiled platform skills"). # A hand edit here is overwritten by the next sync and fails the validator's manifest check, -# so the owners review the sync PRs and route content changes to the apper prompt partials. +# so the owners review the sync PRs and route content changes to the source prompt. /skills/base44-platform-*/ @base44/devs -/APPER_COMMIT @base44/devs +/SOURCE_COMMIT @base44/devs /scripts/validate-base44-platform.mjs @base44/devs /.github/workflows/validate-base44-platform.yml @base44/devs /.github/workflows/auto-merge-skill-update.yml @base44/devs diff --git a/.github/workflows/auto-merge-skill-update.yml b/.github/workflows/auto-merge-skill-update.yml index 136aac3..8baa5f3 100644 --- a/.github/workflows/auto-merge-skill-update.yml +++ b/.github/workflows/auto-merge-skill-update.yml @@ -1,8 +1,8 @@ name: Auto-merge compiled skill updates -# A nightly sync PR from base44-dev/apper (label `auto-skill-update`, opened by the +# A sync PR from the Base44 skills compiler (label `auto-skill-update`, opened by the # base44-github-actions app, the same bot sync-cli-skill.yml / sync-sdk-skill.yml trust) that -# touches only the compiled `skills/base44-platform-*` tree and the APPER_COMMIT pin is merged +# touches only the compiled `skills/base44-platform-*` tree and the SOURCE_COMMIT pin is merged # automatically once `Validate base44-platform skills` passes. Anything else with the label (the # CLI / SDK sync PRs, a hand-made PR) is left for a human. # @@ -33,7 +33,7 @@ jobs: run: | set -euo pipefail git diff --name-only "$BASE" "$HEAD" > changed.txt - if grep -vE '^(skills/base44-platform-[^/]+/|APPER_COMMIT$)' changed.txt; then + if grep -vE '^(skills/base44-platform-[^/]+/|SOURCE_COMMIT$)' changed.txt; then echo "::error::the PR changes files outside the compiled tree; leaving it for a human"; exit 1 fi echo "$(wc -l < changed.txt) generated files changed" diff --git a/.github/workflows/notify-slack-skill-sync.yml b/.github/workflows/notify-slack-skill-sync.yml index 8d2a539..85288cf 100644 --- a/.github/workflows/notify-slack-skill-sync.yml +++ b/.github/workflows/notify-slack-skill-sync.yml @@ -1,12 +1,12 @@ name: Notify Slack on skill-sync PRs # When an auto-generated skill-sync PR opens (base44-cli / base44-sdk, or the nightly -# base44-platform-* tree compiled from base44-dev/apper), post a +# base44-platform-* tree compiled from the Base44 builder prompt), post a # message to the team Slack channel so someone remembers to review it. Fires # only for skill-update PRs; anything else is ignored. # # Discriminator: the `auto-skill-update` label. The sync workflows -# (sync-cli-skill.yml / sync-sdk-skill.yml here, agent-skills-sync.yml in apper) create their PRs with this label, +# (sync-cli-skill.yml / sync-sdk-skill.yml here, the platform-skills sync at Base44) create their PRs with this label, # so it is present in the `opened` event payload. This is a stable, explicit # contract — independent of the PR title wording or which bot authored it. # diff --git a/.github/workflows/validate-base44-platform.yml b/.github/workflows/validate-base44-platform.yml index 484cb65..1688b49 100644 --- a/.github/workflows/validate-base44-platform.yml +++ b/.github/workflows/validate-base44-platform.yml @@ -1,15 +1,15 @@ name: Validate base44-platform skills -# The `skills/base44-platform-*` tree is compiled by base44-dev/apper (backend/scripts/agent_skills) +# The `skills/base44-platform-*` tree is compiled from the Base44 builder prompt # and synced here nightly. This check proves a PR carries exactly what the compiler emitted: -# headers, manifest hashes, frontmatter, links and the APPER_COMMIT pin. Make it a required check +# headers, manifest hashes, frontmatter, links and the SOURCE_COMMIT pin. Make it a required check # on main, so the auto-merge workflow only merges a tree that passed it. on: pull_request: paths: - "skills/base44-platform-*/**" - - "APPER_COMMIT" + - "SOURCE_COMMIT" - "scripts/validate-base44-platform.mjs" - ".github/workflows/validate-base44-platform.yml" diff --git a/APPER_COMMIT b/APPER_COMMIT deleted file mode 100644 index b347f10..0000000 --- a/APPER_COMMIT +++ /dev/null @@ -1 +0,0 @@ -816ecd2112cd23154c79b2ee6c24b8486e87d8af diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f52fded..1374d96 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -120,46 +120,36 @@ npx skills add . --skill your-skill-name -a cursor ## Compiled platform skills (`base44-platform-*`) The `skills/base44-platform-*` directories are **generated**. Their source is the Base44 builder's -system prompt tree in [base44-dev/apper](https://github.com/base44-dev/apper) -(`backend/app/prompts/`), rendered for an external coding agent by the compiler in -`backend/scripts/agent_skills` and published here by apper's nightly `agent-skills-sync` workflow. -They describe the platform exactly as the in-editor AI learns it, one reference per app shape. +own system prompt, rendered for an external coding agent by Base44's internal compiler and +published here by a sync workflow that Base44 runs by hand. They describe the platform exactly as +the in-editor AI learns it, one reference per app shape. ### How the pipeline works -1. **Nightly, in apper**: the compiler renders every prompt partial in the capability matrix - (`backend/app/user_apps/agent_skills/matrix.py`) with `surface="external"`, validates the result - (deterministic output, no builder-tool vocabulary, manifest, links) and opens one PR here on the - branch `auto/base44-platform-skills`, labeled `auto-skill-update`. A night with no prompt change - opens nothing. +1. **At Base44**: the compiler renders every prompt partial in its capability matrix for the + external surface, drops passages marked internal, and validates the result (deterministic output, + no builder-tool vocabulary, no internal names, manifest, links). An IP review then reads the tree + that would be published; only a tree it clears, or one a Base44 engineer has settled, is synced. + The sync opens one PR here on the branch `auto/base44-platform-skills`, labeled + `auto-skill-update`, with the review's report in its body. No change, no PR. 2. **In this repo**: `Validate base44-platform skills` (`scripts/validate-base44-platform.mjs`) checks that the PR carries exactly what the compiler emitted: the generated-file header on every file, sha256 hashes matching `skills/base44-platform-manifest.json`, frontmatter whose `name` - equals the directory name, resolving links, and a 40-hex `APPER_COMMIT` pin. + equals the directory name, resolving links, and a 40-hex `SOURCE_COMMIT` pin. 3. `Auto-merge compiled skill updates` enables squash auto-merge on a labeled PR opened by - `base44-github-actions[bot]` whose diff is confined to `skills/base44-platform-*/**` and `APPER_COMMIT`. Everything else - waits for a human. The Slack notifier posts the PR like any other skill-sync PR. + `base44-github-actions[bot]` whose diff is confined to `skills/base44-platform-*/**` and + `SOURCE_COMMIT`. A PR the review opened as a draft waits for a human; so does everything else. + The Slack notifier posts the PR like any other skill-sync PR. ### How to change a compiled skill Do not edit these files here: the validator fails on a hand edit, and the next sync overwrites it. -Change the source instead: - -- **Wording that is wrong for every agent** (builder and external alike): edit the prompt partial in - apper under `backend/app/prompts/`. The nightly sync carries it here. -- **Wording that only an external agent needs** (a builder-tool instruction with no external - equivalent, a pointer to the MCP / CLI surface): use the `external` branch inside the partial, - `{% if external %}…{% else %}…{% endif %}`. The builder's render stays byte-identical and is - pinned by hash tests. The convention is documented in apper's `backend/app/prompts/CLAUDE.md` - (Rule 4). -- **A new capability, app shape or reference file**: add it to the matrix in apper and classify the - partial there. Nothing in this repo needs to change; the sync adds the directory. -- **A flag that reached 100%** but still gates prompt text: add it to - `backend/app/user_apps/agent_skills/shipped_overrides.json` in apper, so the skills describe the - shipped arm. - -`APPER_COMMIT` names the apper commit the current tree was compiled from; the sync PR body lists -the prompt-source commits since the previous pin. +Open an issue describing the wrong or missing content, or, as a Base44 engineer, change the source +prompt: wording wrong for every agent is fixed in the shared partial; wording only an external agent +needs goes in the partial's external branch; a new capability, app shape or reference is added to the +matrix; a passage that must not be public is marked internal. The next sync carries it here. + +`SOURCE_COMMIT` names the source commit the current tree was compiled from. ### Which reference an agent should read @@ -171,13 +161,12 @@ paths to read. ### Operating the pipeline (maintainers) -- The sync PR is opened by the `base44-github-actions` GitHub App (id 2616622), the same app the - CLI and SDK release workflows use to dispatch here. Apper reads it from - `vars.BASE44_GITHUB_ACTIONS_APP_ID` and `secrets.BASE44_GITHUB_ACTIONS_APP_PRIVATE_KEY`. +- The sync PR is opened by the `base44-github-actions` GitHub App, the same app the CLI and SDK + release workflows use to dispatch here. - Auto-merge must be enabled in the repository settings, with `Validate base44-platform skills` as a required check on `main`. - `.github/CODEOWNERS` routes the generated tree and the two workflows to the owning team. -- To rerun a sync by hand, dispatch apper's `Agent skills sync` workflow (it has a dry-run input). +- Syncs are manual and run from Base44's side; there is no schedule in this repo. ## Questions? diff --git a/README.md b/README.md index ae68772..575e33b 100644 --- a/README.md +++ b/README.md @@ -69,20 +69,20 @@ npx skills add base44/skills --skill base44-remote-dev --skill base44-sandbox -- | [`base44-troubleshooter`](skills/base44-troubleshooter/SKILL.md) | Troubleshoot production issues using backend function logs. Use when investigating app errors or diagnosing production problems. | | [`base44-remote-dev`](skills/base44-remote-dev/SKILL.md) | Develop a Base44 app remotely from your own coding agent by connecting it to the Base44 sandbox over MCP or the `base44 sandbox` CLI. | | [`base44-sandbox`](skills/base44-sandbox/SKILL.md) | Author Base44 app code inside the cloud sandbox — no deploy/push; writing a resource file (function, actor, entity, agent) into the sandbox is what ships it. | -| [`base44-platform-platform`](skills/base44-platform-platform/SKILL.md) | Platform concepts for a Base44 app: entities, functions or server code, workflows, file structure, authentication and user invites, per app type. Compiled nightly from the Base44 builder's own prompt. | -| [`base44-platform-entities`](skills/base44-platform-entities/SKILL.md) | Entity schemas, the entity SDK and row-level security rules, as the Base44 builder teaches them. Compiled nightly from the Base44 builder's own prompt. | -| [`base44-platform-backend-functions`](skills/base44-platform-backend-functions/SKILL.md) | Backend functions on Cloudflare (or TanStack Start server code): secrets, the SDK on the server, auth, webhooks, testing, the InvokeLLM model table. Compiled nightly from the Base44 builder's own prompt. | -| [`base44-platform-workflows`](skills/base44-platform-workflows/SKILL.md) | Workflows: trigger-driven `.jsonc` processes under `base44/workflows/` with the full authoring guide, plus the legacy automations pipeline. Compiled nightly from the Base44 builder's own prompt. | -| [`base44-platform-agents`](skills/base44-platform-agents/SKILL.md) | In-app AI agents: config files, tool permissions, channels, the agents SDK and code agents on the AI gateway. Compiled nightly from the Base44 builder's own prompt. | -| [`base44-platform-actors`](skills/base44-platform-actors/SKILL.md) | Realtime actors: stateful WebSocket rooms for multiplayer, presence, collaborative boards and chat. Compiled nightly from the Base44 builder's own prompt. | -| [`base44-platform-integrations-sdk`](skills/base44-platform-integrations-sdk/SKILL.md) | Built-in integrations (InvokeLLM, SendEmail, file uploads, data extraction) with every Core endpoint schema, plus analytics. Compiled nightly from the Base44 builder's own prompt. | -| [`base44-platform-connectors`](skills/base44-platform-connectors/SKILL.md) | App connectors: OAuth integrations in shared, app-user and BYO-shared modes, and how server code reads their tokens. Compiled nightly from the Base44 builder's own prompt. | -| [`base44-platform-frontend`](skills/base44-platform-frontend/SKILL.md) | Frontend coding standards: conventions, installed packages, routing, Tailwind and design tokens, npm installs, per framework. Compiled nightly from the Base44 builder's own prompt. | -| [`base44-platform-games`](skills/base44-platform-games/SKILL.md) | Phaser 4 and Three.js game references: architecture, assets, analytics and per-topic engine guides. Compiled nightly from the Base44 builder's own prompt. | +| [`base44-platform-platform`](skills/base44-platform-platform/SKILL.md) | Platform concepts for a Base44 app: entities, functions or server code, workflows, file structure, authentication and user invites, per app type. Compiled from the Base44 builder's own prompt. | +| [`base44-platform-entities`](skills/base44-platform-entities/SKILL.md) | Entity schemas, the entity SDK and row-level security rules, as the Base44 builder teaches them. Compiled from the Base44 builder's own prompt. | +| [`base44-platform-backend-functions`](skills/base44-platform-backend-functions/SKILL.md) | Backend functions on Cloudflare (or TanStack Start server code): secrets, the SDK on the server, auth, webhooks, testing, the InvokeLLM model table. Compiled from the Base44 builder's own prompt. | +| [`base44-platform-workflows`](skills/base44-platform-workflows/SKILL.md) | Workflows: trigger-driven `.jsonc` processes under `base44/workflows/` with the full authoring guide, plus the legacy automations pipeline. Compiled from the Base44 builder's own prompt. | +| [`base44-platform-agents`](skills/base44-platform-agents/SKILL.md) | In-app AI agents: config files, tool permissions, channels, the agents SDK and code agents on the AI gateway. Compiled from the Base44 builder's own prompt. | +| [`base44-platform-actors`](skills/base44-platform-actors/SKILL.md) | Realtime actors: stateful WebSocket rooms for multiplayer, presence, collaborative boards and chat. Compiled from the Base44 builder's own prompt. | +| [`base44-platform-integrations-sdk`](skills/base44-platform-integrations-sdk/SKILL.md) | Built-in integrations (InvokeLLM, SendEmail, file uploads, data extraction) with every Core endpoint schema, plus analytics. Compiled from the Base44 builder's own prompt. | +| [`base44-platform-connectors`](skills/base44-platform-connectors/SKILL.md) | App connectors: OAuth integrations in shared, app-user and BYO-shared modes, and how server code reads their tokens. Compiled from the Base44 builder's own prompt. | +| [`base44-platform-frontend`](skills/base44-platform-frontend/SKILL.md) | Frontend coding standards: conventions, installed packages, routing, Tailwind and design tokens, npm installs, per framework. Compiled from the Base44 builder's own prompt. | +| [`base44-platform-games`](skills/base44-platform-games/SKILL.md) | Phaser 4 and Three.js game references: architecture, assets, analytics and per-topic engine guides. Compiled from the Base44 builder's own prompt. | ### Platform skills (compiled) -The `base44-platform-*` skills are generated from the Base44 builder's own system prompt by [base44-dev/apper](https://github.com/base44-dev/apper) and synced here nightly, so they describe the platform exactly as the in-editor AI learns it: one reference per app shape (Vite SPA, TanStack Start, native mobile, 2D/3D game). Ask the platform which shape an app is with `GET /api/apps/{app_id}/agent-profile` (also reachable through the Base44 MCP `execute_api` tool); it returns the exact reference files to read. Do not edit these files by hand: the `APPER_COMMIT` file records the apper commit they were compiled from, and the next sync overwrites them. How the pipeline works and how to change the content: [CONTRIBUTING.md](CONTRIBUTING.md#compiled-platform-skills-base44-platform-). +The `base44-platform-*` skills are generated from the Base44 builder's own system prompt and synced here by Base44, so they describe the platform exactly as the in-editor AI learns it: one reference per app shape (Vite SPA, TanStack Start, native mobile, 2D/3D game). Ask the platform which shape an app is with `GET /api/apps/{app_id}/agent-profile` (also reachable through the Base44 MCP `execute_api` tool); it returns the exact reference files to read. Do not edit these files by hand: the `SOURCE_COMMIT` file records the source commit they were compiled from, and the next sync overwrites them. How the pipeline works and how to change the content: [CONTRIBUTING.md](CONTRIBUTING.md#compiled-platform-skills-base44-platform-). ## About Agent Skills diff --git a/SOURCE_COMMIT b/SOURCE_COMMIT new file mode 100644 index 0000000..ca72e8a --- /dev/null +++ b/SOURCE_COMMIT @@ -0,0 +1 @@ +7acabe324ef52de6aaf6a937b56eb5ac74d64a76 diff --git a/scripts/validate-base44-platform.mjs b/scripts/validate-base44-platform.mjs index 101bae3..c2156b5 100644 --- a/scripts/validate-base44-platform.mjs +++ b/scripts/validate-base44-platform.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -// Validates the compiled `skills/base44-platform-*` tree that base44-dev/apper publishes nightly. -// The compiler (apper: backend/scripts/agent_skills) owns the content; this proves a PR carries +// Validates the compiled `skills/base44-platform-*` tree that Base44 publishes from its builder prompt. +// The compiler at Base44 owns the content; this proves a PR carries // exactly what it emitted: every generated file bears the header, is listed in the manifest with // a matching sha256, has valid frontmatter, and links only to files that exist. @@ -74,8 +74,8 @@ async function main() { } } - const pin = (await fs.readFile(path.join(repoRoot, "APPER_COMMIT"), "utf8").catch(() => "")).trim(); - if (!/^[0-9a-f]{40}$/.test(pin)) errors.push("APPER_COMMIT must hold the 40-hex apper commit the tree was compiled from"); + const pin = (await fs.readFile(path.join(repoRoot, "SOURCE_COMMIT"), "utf8").catch(() => "")).trim(); + if (!/^[0-9a-f]{40}$/.test(pin)) errors.push("SOURCE_COMMIT must hold the 40-hex source commit the tree was compiled from"); if (errors.length) { console.error("Validation failed:"); diff --git a/skills/base44-platform-actors/SKILL.md b/skills/base44-platform-actors/SKILL.md index 5c051df..c565c20 100644 --- a/skills/base44-platform-actors/SKILL.md +++ b/skills/base44-platform-actors/SKILL.md @@ -2,9 +2,9 @@ name: base44-platform-actors description: "Base44 realtime actors: stateful server rooms over WebSockets (Durable Objects) under base44/actors/<Name>/entry.ts for multiplayer, collaborative boards, presence, live cursors, chat rooms and auctions, with the actor contract, rules and the React client. Use when users interact live in one shared session." --- -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 actors One actor instance per room id; the actor is authoritative, clients send operations. Writing the entry file deploys the actor. diff --git a/skills/base44-platform-actors/references/mobile.md b/skills/base44-platform-actors/references/mobile.md index 558deba..6d166d7 100644 --- a/skills/base44-platform-actors/references/mobile.md +++ b/skills/base44-platform-actors/references/mobile.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 actors: Realtime actors (mobile apps) <realtime_actors> diff --git a/skills/base44-platform-actors/references/web.md b/skills/base44-platform-actors/references/web.md index 058bbc5..7c92138 100644 --- a/skills/base44-platform-actors/references/web.md +++ b/skills/base44-platform-actors/references/web.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 actors: Realtime actors (web apps) <realtime_actors> diff --git a/skills/base44-platform-agents/SKILL.md b/skills/base44-platform-agents/SKILL.md index fca6a73..c269ebe 100644 --- a/skills/base44-platform-agents/SKILL.md +++ b/skills/base44-platform-agents/SKILL.md @@ -2,9 +2,9 @@ name: base44-platform-agents description: "Base44 in-app AI agents: JSON agent configs under base44/agents/ with entity, backend-function or server-route tools, WhatsApp and Telegram channels, the agents SDK for conversation UIs, and code agents (an agent loop the app runs on the AI gateway). Use when the app needs a conversational assistant or programmable AI." --- -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 agents An agent is a `.jsonc` file under `base44/agents/`; writing it defines the agent. Build the conversation UI with the agents SDK, or run a code agent on the AI gateway. diff --git a/skills/base44-platform-agents/references/web-spa.md b/skills/base44-platform-agents/references/web-spa.md index b10011d..450f02e 100644 --- a/skills/base44-platform-agents/references/web-spa.md +++ b/skills/base44-platform-agents/references/web-spa.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 agents: Agents (Vite SPA: tools are backend functions) <base44_agents_introduction> @@ -49,7 +49,7 @@ import { base44 } from "@/api/base44Client"; </channels> <agent_ui> -Without an in-app UI, an agent is only usable from the dashboard/backoffice — so when you build an agent, also build the UI for its conversations in the app. +Without an in-app UI, an agent is only usable from the dashboard — so when you build an agent, also build the UI for its conversations in the app. <agents_sdk> Manage conversations, add messages, and subscribe to updates: diff --git a/skills/base44-platform-agents/references/web-tanstack.md b/skills/base44-platform-agents/references/web-tanstack.md index c763cc6..5ea2cf8 100644 --- a/skills/base44-platform-agents/references/web-tanstack.md +++ b/skills/base44-platform-agents/references/web-tanstack.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 agents: Agents (TanStack Start: tools are server routes) <base44_agents_introduction> @@ -51,7 +51,7 @@ import { base44 } from "@/api/base44Client"; </channels> <agent_ui> -Without an in-app UI, an agent is only usable from the dashboard/backoffice — so when you build an agent, also build the UI for its conversations in the app. +Without an in-app UI, an agent is only usable from the dashboard — so when you build an agent, also build the UI for its conversations in the app. <agents_sdk> Manage conversations, add messages, and subscribe to updates: diff --git a/skills/base44-platform-backend-functions/SKILL.md b/skills/base44-platform-backend-functions/SKILL.md index cbf6c65..52273a2 100644 --- a/skills/base44-platform-backend-functions/SKILL.md +++ b/skills/base44-platform-backend-functions/SKILL.md @@ -2,9 +2,9 @@ name: base44-platform-backend-functions description: "Base44 server-side code: backend functions (HTTP handlers in base44/functions/<name>/entry.ts on Cloudflare Workers) for Vite SPA apps, or TanStack Start server functions and server routes. Secrets, the Base44 SDK on the server, auth and service role, webhooks, admin-only handlers, frontend invocation, testing, and the InvokeLLM model table. Use when the app needs external APIs, privileged logic or scheduled work." --- -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 backend functions A Vite SPA app runs server code as backend functions; a TanStack Start app runs it as server functions and server routes inside its own project. Writing the file deploys it. diff --git a/skills/base44-platform-backend-functions/references/mobile.md b/skills/base44-platform-backend-functions/references/mobile.md index d371fda..116504e 100644 --- a/skills/base44-platform-backend-functions/references/mobile.md +++ b/skills/base44-platform-backend-functions/references/mobile.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 backend functions: Backend functions (mobile apps) When a user asks you to integrate with an external API that has no built-in integration, you can write custom backend functions to integrate with the API. diff --git a/skills/base44-platform-backend-functions/references/web-spa.md b/skills/base44-platform-backend-functions/references/web-spa.md index 920466d..fbe7b1b 100644 --- a/skills/base44-platform-backend-functions/references/web-spa.md +++ b/skills/base44-platform-backend-functions/references/web-spa.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 backend functions: Backend functions (Vite SPA) <overview> diff --git a/skills/base44-platform-backend-functions/references/web-tanstack.md b/skills/base44-platform-backend-functions/references/web-tanstack.md index b48b874..369009f 100644 --- a/skills/base44-platform-backend-functions/references/web-tanstack.md +++ b/skills/base44-platform-backend-functions/references/web-tanstack.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 backend functions: Server functions and server routes (TanStack Start) <overview> diff --git a/skills/base44-platform-connectors/SKILL.md b/skills/base44-platform-connectors/SKILL.md index 55ee847..8063e02 100644 --- a/skills/base44-platform-connectors/SKILL.md +++ b/skills/base44-platform-connectors/SKILL.md @@ -2,9 +2,9 @@ name: base44-platform-connectors description: "Base44 app connectors: OAuth integrations with external services (Google, Slack, GitHub and more) in shared, app-user and BYO-shared modes, how to connect one from an external agent, and how server code reads the token with getConnection. Use when the app must act on a user's or the builder's account at a third-party service." --- -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 connectors Connectors are configured against the app, not authored as files: list the catalog, connect with the complete scope set, then read tokens in server code. diff --git a/skills/base44-platform-connectors/references/mobile.md b/skills/base44-platform-connectors/references/mobile.md index 9a115cb..d05f0db 100644 --- a/skills/base44-platform-connectors/references/mobile.md +++ b/skills/base44-platform-connectors/references/mobile.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 connectors: App connectors (mobile apps) <app_connectors> diff --git a/skills/base44-platform-connectors/references/web-spa.md b/skills/base44-platform-connectors/references/web-spa.md index 72b775d..3191b87 100644 --- a/skills/base44-platform-connectors/references/web-spa.md +++ b/skills/base44-platform-connectors/references/web-spa.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 connectors: App connectors (Vite SPA) <app_connectors> diff --git a/skills/base44-platform-connectors/references/web-tanstack.md b/skills/base44-platform-connectors/references/web-tanstack.md index 8959f7e..43d397e 100644 --- a/skills/base44-platform-connectors/references/web-tanstack.md +++ b/skills/base44-platform-connectors/references/web-tanstack.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 connectors: App connectors (TanStack Start) <app_connectors> diff --git a/skills/base44-platform-entities/SKILL.md b/skills/base44-platform-entities/SKILL.md index bb161db..ce3c0db 100644 --- a/skills/base44-platform-entities/SKILL.md +++ b/skills/base44-platform-entities/SKILL.md @@ -2,9 +2,9 @@ name: base44-platform-entities description: "Base44 entities: the JSON-schema data models under base44/entities/, the entity SDK, and row-level security (RLS) rules. Use when adding or changing stored data, queries, or per-user access rules in a Base44 app." --- -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 entities Entities are JSON-schema objects stored as `base44/entities/<Name>.jsonc`; writing the file is what defines the data model. The platform applies each entity's `rls` rules on every app-user request. diff --git a/skills/base44-platform-entities/references/mobile-and-games.md b/skills/base44-platform-entities/references/mobile-and-games.md index 27f8bd5..290ee8a 100644 --- a/skills/base44-platform-entities/references/mobile-and-games.md +++ b/skills/base44-platform-entities/references/mobile-and-games.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 entities: Entities and the entity SDK (mobile and game apps) Entities are defined as json schema objects that define the shape of the data that is stored in the app. diff --git a/skills/base44-platform-entities/references/rls.md b/skills/base44-platform-entities/references/rls.md index 544c020..836d853 100644 --- a/skills/base44-platform-entities/references/rls.md +++ b/skills/base44-platform-entities/references/rls.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 entities: Row-level security (RLS) <rls_authoring_guide> diff --git a/skills/base44-platform-entities/references/web.md b/skills/base44-platform-entities/references/web.md index 49e0a15..2252017 100644 --- a/skills/base44-platform-entities/references/web.md +++ b/skills/base44-platform-entities/references/web.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 entities: Entities and the entity SDK (web apps) Entities = JSON schema objects defining stored data. Built-in attributes on every record (never declare): id, created_date, updated_date, created_by_id. diff --git a/skills/base44-platform-frontend/SKILL.md b/skills/base44-platform-frontend/SKILL.md index 1ab3092..faede2f 100644 --- a/skills/base44-platform-frontend/SKILL.md +++ b/skills/base44-platform-frontend/SKILL.md @@ -2,9 +2,9 @@ name: base44-platform-frontend description: "Frontend coding standards for Base44 web apps: code conventions, the installed packages, routing (React Router in the Vite SPA, file routes in TanStack Start), Tailwind and design tokens, styling and componentization rules, npm installs, and how files are edited in the sandbox. Use when writing or changing pages, components or routes." --- -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 frontend The builder's own frontend rules for Base44 web apps. Pick the reference for the app's framework; the TanStack reference ends with a worked example. diff --git a/skills/base44-platform-frontend/references/mobile.md b/skills/base44-platform-frontend/references/mobile.md index 50e250f..c0af9a8 100644 --- a/skills/base44-platform-frontend/references/mobile.md +++ b/skills/base44-platform-frontend/references/mobile.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 frontend: Native mobile app (Expo, React Native, NativeWind) When coding, you are an exceptional software developer, expert in React Native and Expo mobile development. diff --git a/skills/base44-platform-frontend/references/web-spa.md b/skills/base44-platform-frontend/references/web-spa.md index 018e858..36c2b1d 100644 --- a/skills/base44-platform-frontend/references/web-spa.md +++ b/skills/base44-platform-frontend/references/web-spa.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 frontend: Web app, Vite SPA You develop pages and, when needed, components or the app's layout. diff --git a/skills/base44-platform-frontend/references/web-tanstack.md b/skills/base44-platform-frontend/references/web-tanstack.md index 4f31cbd..4a93815 100644 --- a/skills/base44-platform-frontend/references/web-tanstack.md +++ b/skills/base44-platform-frontend/references/web-tanstack.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 frontend: Web app, TanStack Start You develop pages and, when needed, components or the app's layout. diff --git a/skills/base44-platform-games/SKILL.md b/skills/base44-platform-games/SKILL.md index 122bd54..5a1d737 100644 --- a/skills/base44-platform-games/SKILL.md +++ b/skills/base44-platform-games/SKILL.md @@ -2,9 +2,9 @@ name: base44-platform-games description: "Base44 games: Phaser 4 (2D) and Three.js via React Three Fiber (3D) reference material for game apps built on Base44. Use when implementing scenes, physics, input, cameras, assets, HUD or game state in a Base44 game." --- -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games A Base44 game is a React app with the game layer under `src/game/`; React owns the screen-space overlay and the Base44 SDK, the engine owns gameplay. Each topic reference covers one engine area. diff --git a/skills/base44-platform-games/references/game-2d.md b/skills/base44-platform-games/references/game-2d.md index 92504ef..46b4e97 100644 --- a/skills/base44-platform-games/references/game-2d.md +++ b/skills/base44-platform-games/references/game-2d.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: 2D game (Phaser 4): architecture, assets, analytics and the Phaser reference <game_architecture> diff --git a/skills/base44-platform-games/references/game-3d.md b/skills/base44-platform-games/references/game-3d.md index 95c5159..4d1cfa6 100644 --- a/skills/base44-platform-games/references/game-3d.md +++ b/skills/base44-platform-games/references/game-3d.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: 3D game (Three.js): architecture, assets, analytics and the Three.js reference <game_architecture> diff --git a/skills/base44-platform-games/references/phaser/actions_and_utilities.md b/skills/base44-platform-games/references/phaser/actions_and_utilities.md index 6d5170f..9a0d56e 100644 --- a/skills/base44-platform-games/references/phaser/actions_and_utilities.md +++ b/skills/base44-platform-games/references/phaser/actions_and_utilities.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: actions and utilities <actions_and_utilities> diff --git a/skills/base44-platform-games/references/phaser/animations.md b/skills/base44-platform-games/references/phaser/animations.md index 5cbea05..5b9855c 100644 --- a/skills/base44-platform-games/references/phaser/animations.md +++ b/skills/base44-platform-games/references/phaser/animations.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: animations <animations> diff --git a/skills/base44-platform-games/references/phaser/audio_and_sound.md b/skills/base44-platform-games/references/phaser/audio_and_sound.md index 1a11c36..6975f5a 100644 --- a/skills/base44-platform-games/references/phaser/audio_and_sound.md +++ b/skills/base44-platform-games/references/phaser/audio_and_sound.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: audio and sound <audio_and_sound> diff --git a/skills/base44-platform-games/references/phaser/cameras.md b/skills/base44-platform-games/references/phaser/cameras.md index 8bdc6c3..10a1dd2 100644 --- a/skills/base44-platform-games/references/phaser/cameras.md +++ b/skills/base44-platform-games/references/phaser/cameras.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: cameras <cameras> diff --git a/skills/base44-platform-games/references/phaser/curves_and_paths.md b/skills/base44-platform-games/references/phaser/curves_and_paths.md index 2ea4ceb..0e04259 100644 --- a/skills/base44-platform-games/references/phaser/curves_and_paths.md +++ b/skills/base44-platform-games/references/phaser/curves_and_paths.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: curves and paths <curves_and_paths> diff --git a/skills/base44-platform-games/references/phaser/data_manager.md b/skills/base44-platform-games/references/phaser/data_manager.md index 29a37e9..dc3c832 100644 --- a/skills/base44-platform-games/references/phaser/data_manager.md +++ b/skills/base44-platform-games/references/phaser/data_manager.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: data manager <data_manager> diff --git a/skills/base44-platform-games/references/phaser/events_system.md b/skills/base44-platform-games/references/phaser/events_system.md index fb70dbd..f9f1058 100644 --- a/skills/base44-platform-games/references/phaser/events_system.md +++ b/skills/base44-platform-games/references/phaser/events_system.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: events system <events_system> diff --git a/skills/base44-platform-games/references/phaser/filters_and_postfx.md b/skills/base44-platform-games/references/phaser/filters_and_postfx.md index e0c9eeb..5e5a00c 100644 --- a/skills/base44-platform-games/references/phaser/filters_and_postfx.md +++ b/skills/base44-platform-games/references/phaser/filters_and_postfx.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: filters and postfx <filters_and_postfx> diff --git a/skills/base44-platform-games/references/phaser/game_object_components.md b/skills/base44-platform-games/references/phaser/game_object_components.md index ffa8524..1bfb175 100644 --- a/skills/base44-platform-games/references/phaser/game_object_components.md +++ b/skills/base44-platform-games/references/phaser/game_object_components.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: game object components <game_object_components> diff --git a/skills/base44-platform-games/references/phaser/game_setup_and_config.md b/skills/base44-platform-games/references/phaser/game_setup_and_config.md index b4c0a9a..2a5ab5a 100644 --- a/skills/base44-platform-games/references/phaser/game_setup_and_config.md +++ b/skills/base44-platform-games/references/phaser/game_setup_and_config.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: game setup and config <game_setup_and_config> diff --git a/skills/base44-platform-games/references/phaser/geometry_and_math.md b/skills/base44-platform-games/references/phaser/geometry_and_math.md index 1278033..0fd3d6c 100644 --- a/skills/base44-platform-games/references/phaser/geometry_and_math.md +++ b/skills/base44-platform-games/references/phaser/geometry_and_math.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: geometry and math <geometry_and_math> diff --git a/skills/base44-platform-games/references/phaser/graphics_and_shapes.md b/skills/base44-platform-games/references/phaser/graphics_and_shapes.md index 5cc5dba..8be07e5 100644 --- a/skills/base44-platform-games/references/phaser/graphics_and_shapes.md +++ b/skills/base44-platform-games/references/phaser/graphics_and_shapes.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: graphics and shapes <graphics_and_shapes> diff --git a/skills/base44-platform-games/references/phaser/groups_and_containers.md b/skills/base44-platform-games/references/phaser/groups_and_containers.md index 8b7a042..8c447a4 100644 --- a/skills/base44-platform-games/references/phaser/groups_and_containers.md +++ b/skills/base44-platform-games/references/phaser/groups_and_containers.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: groups and containers <groups_and_containers> diff --git a/skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md b/skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md index a425d9c..6986b2e 100644 --- a/skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md +++ b/skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: input keyboard mouse touch <input_keyboard_mouse_touch> diff --git a/skills/base44-platform-games/references/phaser/loading_assets.md b/skills/base44-platform-games/references/phaser/loading_assets.md index fe867c6..4f6fa8b 100644 --- a/skills/base44-platform-games/references/phaser/loading_assets.md +++ b/skills/base44-platform-games/references/phaser/loading_assets.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: loading assets <loading_assets> diff --git a/skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md b/skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md index e17c558..e9f12c0 100644 --- a/skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md +++ b/skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: mesh and 3d elements <mesh_and_3d_elements> diff --git a/skills/base44-platform-games/references/phaser/particles.md b/skills/base44-platform-games/references/phaser/particles.md index 0a063c0..94dd3fc 100644 --- a/skills/base44-platform-games/references/phaser/particles.md +++ b/skills/base44-platform-games/references/phaser/particles.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: particles <particles> diff --git a/skills/base44-platform-games/references/phaser/physics_arcade.md b/skills/base44-platform-games/references/phaser/physics_arcade.md index 48d0ad9..204a44d 100644 --- a/skills/base44-platform-games/references/phaser/physics_arcade.md +++ b/skills/base44-platform-games/references/phaser/physics_arcade.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: physics arcade <physics_arcade> diff --git a/skills/base44-platform-games/references/phaser/physics_matter.md b/skills/base44-platform-games/references/phaser/physics_matter.md index 05f53f6..000c5c7 100644 --- a/skills/base44-platform-games/references/phaser/physics_matter.md +++ b/skills/base44-platform-games/references/phaser/physics_matter.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: physics matter <physics_matter> diff --git a/skills/base44-platform-games/references/phaser/render_textures.md b/skills/base44-platform-games/references/phaser/render_textures.md index 08a7f57..1e50e19 100644 --- a/skills/base44-platform-games/references/phaser/render_textures.md +++ b/skills/base44-platform-games/references/phaser/render_textures.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: render textures <render_textures> diff --git a/skills/base44-platform-games/references/phaser/scale_and_responsive.md b/skills/base44-platform-games/references/phaser/scale_and_responsive.md index 971a7d3..becf79d 100644 --- a/skills/base44-platform-games/references/phaser/scale_and_responsive.md +++ b/skills/base44-platform-games/references/phaser/scale_and_responsive.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: scale and responsive <scale_and_responsive> diff --git a/skills/base44-platform-games/references/phaser/scenes.md b/skills/base44-platform-games/references/phaser/scenes.md index 858f3cf..cdf82a0 100644 --- a/skills/base44-platform-games/references/phaser/scenes.md +++ b/skills/base44-platform-games/references/phaser/scenes.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: scenes <scenes> diff --git a/skills/base44-platform-games/references/phaser/sprites_and_images.md b/skills/base44-platform-games/references/phaser/sprites_and_images.md index 706ea10..85fbb75 100644 --- a/skills/base44-platform-games/references/phaser/sprites_and_images.md +++ b/skills/base44-platform-games/references/phaser/sprites_and_images.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: sprites and images <sprites_and_images> diff --git a/skills/base44-platform-games/references/phaser/text_and_bitmaptext.md b/skills/base44-platform-games/references/phaser/text_and_bitmaptext.md index 965c3d7..972c4dc 100644 --- a/skills/base44-platform-games/references/phaser/text_and_bitmaptext.md +++ b/skills/base44-platform-games/references/phaser/text_and_bitmaptext.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: text and bitmaptext <text_and_bitmaptext> diff --git a/skills/base44-platform-games/references/phaser/tilemaps.md b/skills/base44-platform-games/references/phaser/tilemaps.md index 2087a8a..06fc6d1 100644 --- a/skills/base44-platform-games/references/phaser/tilemaps.md +++ b/skills/base44-platform-games/references/phaser/tilemaps.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: tilemaps <tilemaps> diff --git a/skills/base44-platform-games/references/phaser/time_and_timers.md b/skills/base44-platform-games/references/phaser/time_and_timers.md index 32a4bf6..bb7f8eb 100644 --- a/skills/base44-platform-games/references/phaser/time_and_timers.md +++ b/skills/base44-platform-games/references/phaser/time_and_timers.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: time and timers <time_and_timers> diff --git a/skills/base44-platform-games/references/phaser/tweens.md b/skills/base44-platform-games/references/phaser/tweens.md index 5520bcf..34d14f7 100644 --- a/skills/base44-platform-games/references/phaser/tweens.md +++ b/skills/base44-platform-games/references/phaser/tweens.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Phaser 4: tweens <tweens> diff --git a/skills/base44-platform-games/references/threejs/assets_models_textures.md b/skills/base44-platform-games/references/threejs/assets_models_textures.md index cc698f9..43efa13 100644 --- a/skills/base44-platform-games/references/threejs/assets_models_textures.md +++ b/skills/base44-platform-games/references/threejs/assets_models_textures.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: assets models textures <assets_models_textures> diff --git a/skills/base44-platform-games/references/threejs/cameras.md b/skills/base44-platform-games/references/threejs/cameras.md index 0dc7bdb..fb62ae8 100644 --- a/skills/base44-platform-games/references/threejs/cameras.md +++ b/skills/base44-platform-games/references/threejs/cameras.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: cameras <cameras> diff --git a/skills/base44-platform-games/references/threejs/character_controller.md b/skills/base44-platform-games/references/threejs/character_controller.md index bb888af..18758d0 100644 --- a/skills/base44-platform-games/references/threejs/character_controller.md +++ b/skills/base44-platform-games/references/threejs/character_controller.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: character controller <character_controller> diff --git a/skills/base44-platform-games/references/threejs/enemies_and_movement.md b/skills/base44-platform-games/references/threejs/enemies_and_movement.md index 595f93f..66f36a3 100644 --- a/skills/base44-platform-games/references/threejs/enemies_and_movement.md +++ b/skills/base44-platform-games/references/threejs/enemies_and_movement.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: enemies and movement <enemies_and_movement> diff --git a/skills/base44-platform-games/references/threejs/game_state_and_flow.md b/skills/base44-platform-games/references/threejs/game_state_and_flow.md index 72fc65a..1ee51d1 100644 --- a/skills/base44-platform-games/references/threejs/game_state_and_flow.md +++ b/skills/base44-platform-games/references/threejs/game_state_and_flow.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: game state and flow <game_state_and_flow> diff --git a/skills/base44-platform-games/references/threejs/input_and_controls.md b/skills/base44-platform-games/references/threejs/input_and_controls.md index 38e3947..f278e0f 100644 --- a/skills/base44-platform-games/references/threejs/input_and_controls.md +++ b/skills/base44-platform-games/references/threejs/input_and_controls.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: input and controls <input_and_controls> diff --git a/skills/base44-platform-games/references/threejs/performance.md b/skills/base44-platform-games/references/threejs/performance.md index 1698330..607a091 100644 --- a/skills/base44-platform-games/references/threejs/performance.md +++ b/skills/base44-platform-games/references/threejs/performance.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: performance <performance> diff --git a/skills/base44-platform-games/references/threejs/physics_dynamics.md b/skills/base44-platform-games/references/threejs/physics_dynamics.md index d157d42..69d74ba 100644 --- a/skills/base44-platform-games/references/threejs/physics_dynamics.md +++ b/skills/base44-platform-games/references/threejs/physics_dynamics.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: physics dynamics <physics_dynamics> diff --git a/skills/base44-platform-games/references/threejs/projectiles_and_pooling.md b/skills/base44-platform-games/references/threejs/projectiles_and_pooling.md index 7c127d9..24b0f8e 100644 --- a/skills/base44-platform-games/references/threejs/projectiles_and_pooling.md +++ b/skills/base44-platform-games/references/threejs/projectiles_and_pooling.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: projectiles and pooling <projectiles_and_pooling> diff --git a/skills/base44-platform-games/references/threejs/scene_and_rendering.md b/skills/base44-platform-games/references/threejs/scene_and_rendering.md index 717e225..eed0c08 100644 --- a/skills/base44-platform-games/references/threejs/scene_and_rendering.md +++ b/skills/base44-platform-games/references/threejs/scene_and_rendering.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: scene and rendering <scene_and_rendering> diff --git a/skills/base44-platform-games/references/threejs/text_hud_ui.md b/skills/base44-platform-games/references/threejs/text_hud_ui.md index 82a94bc..cf530a7 100644 --- a/skills/base44-platform-games/references/threejs/text_hud_ui.md +++ b/skills/base44-platform-games/references/threejs/text_hud_ui.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: text hud ui <text_hud_ui> diff --git a/skills/base44-platform-games/references/threejs/triggers_and_collisions.md b/skills/base44-platform-games/references/threejs/triggers_and_collisions.md index 76a6e14..7fe3cfc 100644 --- a/skills/base44-platform-games/references/threejs/triggers_and_collisions.md +++ b/skills/base44-platform-games/references/threejs/triggers_and_collisions.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: triggers and collisions <triggers_and_collisions> diff --git a/skills/base44-platform-games/references/threejs/vfx_and_juice.md b/skills/base44-platform-games/references/threejs/vfx_and_juice.md index 266f196..41c0b84 100644 --- a/skills/base44-platform-games/references/threejs/vfx_and_juice.md +++ b/skills/base44-platform-games/references/threejs/vfx_and_juice.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 games: Three.js: vfx and juice <vfx_and_juice> diff --git a/skills/base44-platform-integrations-sdk/SKILL.md b/skills/base44-platform-integrations-sdk/SKILL.md index dcb38ee..e31937c 100644 --- a/skills/base44-platform-integrations-sdk/SKILL.md +++ b/skills/base44-platform-integrations-sdk/SKILL.md @@ -2,9 +2,9 @@ name: base44-platform-integrations-sdk description: "Base44 built-in integrations on the SDK client: InvokeLLM, SendEmail, file uploads (public and private), signed URLs, data extraction and image generation, with every Core endpoint's input schema, plus analytics event tracking. Use when a feature needs AI, email, files or analytics without a third-party service." --- -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 integrations sdk File uploads and signed URLs are called from the browser SDK; every other Core integration runs in server code on an app with core-integration control, the default for new apps (profile `core_integrations: backend_functions_only`). The endpoint schemas below are the ones the platform serves today. diff --git a/skills/base44-platform-integrations-sdk/references/mobile.md b/skills/base44-platform-integrations-sdk/references/mobile.md index 1d66571..59d1946 100644 --- a/skills/base44-platform-integrations-sdk/references/mobile.md +++ b/skills/base44-platform-integrations-sdk/references/mobile.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 integrations sdk: Integrations SDK (mobile apps) Integrations are packages of functions that are used to interact with external data / services. diff --git a/skills/base44-platform-integrations-sdk/references/web-spa.md b/skills/base44-platform-integrations-sdk/references/web-spa.md index c20e783..9e89681 100644 --- a/skills/base44-platform-integrations-sdk/references/web-spa.md +++ b/skills/base44-platform-integrations-sdk/references/web-spa.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 integrations sdk: Integrations and analytics SDK (Vite SPA) Integrations: function packages for external data/services, on the pre-initialized base44 client: diff --git a/skills/base44-platform-integrations-sdk/references/web-tanstack.md b/skills/base44-platform-integrations-sdk/references/web-tanstack.md index 8906f43..87c1a93 100644 --- a/skills/base44-platform-integrations-sdk/references/web-tanstack.md +++ b/skills/base44-platform-integrations-sdk/references/web-tanstack.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 integrations sdk: Integrations and analytics SDK (TanStack Start) Integrations: function packages for external data/services, on the pre-initialized base44 client: diff --git a/skills/base44-platform-manifest.json b/skills/base44-platform-manifest.json index 4cc85d4..3c8a428 100644 --- a/skills/base44-platform-manifest.json +++ b/skills/base44-platform-manifest.json @@ -1,83 +1,83 @@ { "files": { - "skills/base44-platform-actors/SKILL.md": "a8906d6a448905f839631b80e391f9bd3a982391cc69c181e1f5818888881afd", - "skills/base44-platform-actors/references/mobile.md": "a64520a0d9c6277e2c3289536d8c80f240efbf996589bf463153e78dbf6d0e5f", - "skills/base44-platform-actors/references/web.md": "7a4e32c4911952225c7d28d85479c480ccee5df72ce99d7fcede7dcb64d3eb0c", - "skills/base44-platform-agents/SKILL.md": "a426489d2e27dcc78e5dea84aa8f3a7be9dc79563559f3d68328cd9533ad5098", - "skills/base44-platform-agents/references/web-spa.md": "b569d56379dca260f7ab8e0ca767330031ea329e42a2f395c3c5269db372d7ec", - "skills/base44-platform-agents/references/web-tanstack.md": "ce8257bdc0b5c316734f143115f3a7dda8ba8358a61a73c00b4ebd1bc72481a9", - "skills/base44-platform-backend-functions/SKILL.md": "66e96aec70ce4303462465a0baf9f46342b7e60c70267b1c4b23c38b39e31fc6", - "skills/base44-platform-backend-functions/references/mobile.md": "c9adfacd9917d2e72e88066a9f1b266e961a023a5a3e977e3e05e7641cd01253", - "skills/base44-platform-backend-functions/references/web-spa.md": "a24eb530b64b2a868ac168d46bb117711282448ce4b393d4783ee76b55dc798e", - "skills/base44-platform-backend-functions/references/web-tanstack.md": "0b7f4fb263e82a7c22546cadbf54bd959fa88da9fa4da79bacd467bd037f52b9", - "skills/base44-platform-connectors/SKILL.md": "4bb82de0cabd141953434d553a1f1c57754ea07163651ec383ea5f0edb825046", - "skills/base44-platform-connectors/references/mobile.md": "a8640d9156acbe12bc89599da1d91f2985fabdd2970c6224c557dfb2219e4b9c", - "skills/base44-platform-connectors/references/web-spa.md": "ea850bc48c003b7c79ac51dec60dca2b942be1328e177298027dcf464f37ba60", - "skills/base44-platform-connectors/references/web-tanstack.md": "4576eba68f373ca136b8087cd820c29e8771b16bdcc8011e26eb9f74db9ee487", - "skills/base44-platform-entities/SKILL.md": "c84de4b41d8d64b20e4a4522a97422f500eb3554e9ccc48075130fdb8ae90d06", - "skills/base44-platform-entities/references/mobile-and-games.md": "559313916e24dcfb4cc9ba2e250d8c449ac8555ce8e5494d91d8075b86aa1b0d", - "skills/base44-platform-entities/references/rls.md": "d70c3f6aee3f9d4bb4a771078cd70e1c2ddfffa84a4598627b36c13f490e77b0", - "skills/base44-platform-entities/references/web.md": "02f81b350c864455c722c13405cceabdb5b504de396f272aa1e51571a3cf8a01", - "skills/base44-platform-frontend/SKILL.md": "40f9751943bcaac870313afa6b6335bd9d05908a7cee1f82be454f2ada38f8f0", - "skills/base44-platform-frontend/references/mobile.md": "68d1983605b40f812ee6ed6374649763f469681fd8cfd5f198a3e39e26107957", - "skills/base44-platform-frontend/references/web-spa.md": "a019c49df78cbd7f4b458992fc2a6483f10230c8a2f475dc34c035b3af38ae3e", - "skills/base44-platform-frontend/references/web-tanstack.md": "8962af6b15fc2f0cd39a0822189f87eb5558a2bb6809412bf9ac89cd4b9d7df7", - "skills/base44-platform-games/SKILL.md": "a41d9aae095b377e17c35e207b014718f1b674ccf766330d6638dd63b4d92a1c", - "skills/base44-platform-games/references/game-2d.md": "b07667d58c77348b181b45a2d202836362d71d2e21db5569aefa107c5e1b417d", - "skills/base44-platform-games/references/game-3d.md": "6dff6bfc6fb221c0e39fb4517fc230b3c68265e6b25d700c4e7c29fcea6a8946", - "skills/base44-platform-games/references/phaser/actions_and_utilities.md": "0e9b79f431c60f34535de6ed1098162ee9aebc9838a6fc077d2b0144bf66a26c", - "skills/base44-platform-games/references/phaser/animations.md": "2a7bae86a72abd200717da0a6d3022af5226c0701f93e16a50909ed2b516d4d5", - "skills/base44-platform-games/references/phaser/audio_and_sound.md": "0f121856952f0288712b0dd83404a461e855f6cdd590884e2151506b6689b4f3", - "skills/base44-platform-games/references/phaser/cameras.md": "17ba29369c4f42afb0a6594af6a11fdb28e36f47b5c79de3343fb68092c4251c", - "skills/base44-platform-games/references/phaser/curves_and_paths.md": "b59ff3f894e705818598fc3ddfa824f485317fbd1acd08cbdff4b325719b274a", - "skills/base44-platform-games/references/phaser/data_manager.md": "3312a26a0be196c8360484204b37905bea783bf86d08ac309b00e929d6886155", - "skills/base44-platform-games/references/phaser/events_system.md": "f376277861973731e778d64d0e3202238aebfb3e91f2e7cccf9024c3ac144eb1", - "skills/base44-platform-games/references/phaser/filters_and_postfx.md": "6f826aeb18359a9de309e1c5855e70d76127d07bad84596cd7a0f8cb9ab5d882", - "skills/base44-platform-games/references/phaser/game_object_components.md": "d96c5b50b23b9255fa3090800af993cf5241dc18207604a6122b0011f4b016ca", - "skills/base44-platform-games/references/phaser/game_setup_and_config.md": "b012a4362069c7cdae769d4c3644f987271cdc867b1848ab714fb6743a24d590", - "skills/base44-platform-games/references/phaser/geometry_and_math.md": "5eb35bc5f7d202a5e4779e27c44b88eeb5f769482617e16cb8b85f0bb3ceb614", - "skills/base44-platform-games/references/phaser/graphics_and_shapes.md": "8962622df9c62ae04651f76351aaeaa4e1cd7ab1b227ee55ee09baa1a4d54519", - "skills/base44-platform-games/references/phaser/groups_and_containers.md": "184a8b901dca19b7c884ab186fec2ffc11e0486c6a361687f20e039673b32152", - "skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md": "d89eeaff3f53ef71851dd2e5ef377aea25004cd904ddcf316a67ac5cc6a56c9b", - "skills/base44-platform-games/references/phaser/loading_assets.md": "089cbbfe2d0ecdbcd32cd968f5e7679a92bcdd739ee1f010c9e409f1b7221b75", - "skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md": "805bbe67ccbc8b6aeac4c8b2350f0e37a916b871205374e40c9fa6678b60c57a", - "skills/base44-platform-games/references/phaser/particles.md": "f54ab6d5affc3dee8b96bb0cc972ef519827c115007b89ff4eed7105ab5f137c", - "skills/base44-platform-games/references/phaser/physics_arcade.md": "aaf41916ce2360b847da4817a4b9d11b79c99ede2facba2f2e0601817f402992", - "skills/base44-platform-games/references/phaser/physics_matter.md": "5710fd82ad60658ba63b9084f747a448940efc5792b7f76601740b5a3c74ccb1", - "skills/base44-platform-games/references/phaser/render_textures.md": "82783870d125fd1f47927a11cfda853275e7c4458183caecce5cd5783233643f", - "skills/base44-platform-games/references/phaser/scale_and_responsive.md": "f4d60bfa9194b4d1c9cac12478d198047ea2747c5b29b8eea8b1eeefca682f8f", - "skills/base44-platform-games/references/phaser/scenes.md": "c0ae75d7013d275ea17d465a36b710eae54dffec988bf10648c5938cb3d644c8", - "skills/base44-platform-games/references/phaser/sprites_and_images.md": "ae6dfb48e6f37932a24da09723246552e27ef68e353fe521e95c186622e9075d", - "skills/base44-platform-games/references/phaser/text_and_bitmaptext.md": "44aa65d76bd320756957a88cb0c2f8487fc4b65decb0c63eb23a1b7f294fdb73", - "skills/base44-platform-games/references/phaser/tilemaps.md": "a29644f606baa35b0043743a837c01fbff74e70b26ce8aef0f16116b22f4ec2d", - "skills/base44-platform-games/references/phaser/time_and_timers.md": "662e293a3da5a6bf161036062488c1e61f55df4b045ef0fca43c8caf89c84469", - "skills/base44-platform-games/references/phaser/tweens.md": "ebd1132bd8525f3235763a79a4864c324fdab768698cc91553eb4ee9d49ef5f4", - "skills/base44-platform-games/references/threejs/assets_models_textures.md": "7054454e4483b511a1fcae96a66b80f8274af3277fd5dbc21c546bd9cfba0499", - "skills/base44-platform-games/references/threejs/cameras.md": "03f3f3b2ff4d557fac36c3b5016932dfb769195ff96ba02d8831bfa979a9fd89", - "skills/base44-platform-games/references/threejs/character_controller.md": "742affe45cba7580f146e65d9b558234986bdadee13824d0abd726649855a2c3", - "skills/base44-platform-games/references/threejs/enemies_and_movement.md": "8e0a6029e1f889e6f73e276650c80470319726abefaf68bee3787828d64f57b6", - "skills/base44-platform-games/references/threejs/game_state_and_flow.md": "4a8ba35f9f1b146f574236d095ee4aa3ed3b75e729fad0dd96405c363098b5c9", - "skills/base44-platform-games/references/threejs/input_and_controls.md": "012e6a4f94c2b3a9f41f33101bcc02a01e836102b4a49800bdccba9903c3ce7a", - "skills/base44-platform-games/references/threejs/performance.md": "dce5ea18f566aabccaa6d2a4ceb7acff4c09e15328fbce2f213f55cadf07922d", - "skills/base44-platform-games/references/threejs/physics_dynamics.md": "6c6b413ebb61de82dce332d552e2ecc4a8f1cf9c55be0a7cc25b1613cd5f071a", - "skills/base44-platform-games/references/threejs/projectiles_and_pooling.md": "d6347da652522fa9d787b9aacc61e298d3347dd8b21b47880057ed2abc5cf597", - "skills/base44-platform-games/references/threejs/scene_and_rendering.md": "04b3c9302c85d51342ff2c176f375b3303cddf0e1cfff105e33e29d34a32c236", - "skills/base44-platform-games/references/threejs/text_hud_ui.md": "4e3f879b4f4f06eb035a2d9cc8d709e66d2178114018c5107e55c883a53a9de5", - "skills/base44-platform-games/references/threejs/triggers_and_collisions.md": "09d686d06ba9a85310250b61113e7c57883a184ae14458899f0458f39cb80be5", - "skills/base44-platform-games/references/threejs/vfx_and_juice.md": "c69797aa5d45def521138729eb08ab8b369c0517371b29abe49637381a39729c", - "skills/base44-platform-integrations-sdk/SKILL.md": "ae75594a282ec160d5ef2b3e691e0528f26191f4daf80469d69a22d8d0ba6c8f", - "skills/base44-platform-integrations-sdk/references/mobile.md": "65e1be881f2a49c6eb1392f0d91bff4e1e4f531bb5bfbd641f474a4d7a495e89", - "skills/base44-platform-integrations-sdk/references/web-spa.md": "ee3729d343a9f0ae8ab7dbd0ffb45aaf29ae2a48ea8a71851e016b500dc9bc6b", - "skills/base44-platform-integrations-sdk/references/web-tanstack.md": "3f9fd4c6dd8be2c8272cbafcadbf888fdde4c7eaebbe1d47437b296636415bef", - "skills/base44-platform-platform/SKILL.md": "244f32d1afeb6a730f1bede1d266d59178cf746c98f5f8d25cb71d4aefa866a7", - "skills/base44-platform-platform/references/game.md": "2ad9fe16f5286582db4ca2c9a201658d4970fae327db4240ae5ad2316ed043bc", - "skills/base44-platform-platform/references/mobile.md": "e6a2cc30e2ea4b355242ddcf21316168df8e71d57c26ee8124d1d08d51c815ad", - "skills/base44-platform-platform/references/web-spa.md": "d1c36c3b8604f5ffcfc45f3ba2427320a3d45aaa4903b341d0825a1a9ce41711", - "skills/base44-platform-platform/references/web-tanstack.md": "847c72381358b9dba8607835d7d46fcf955fae65af9f73d87baa7b74ae7c390a", - "skills/base44-platform-workflows/SKILL.md": "55fb8de2004bbbcb4369ac58b91acd5613c8b750eed79e4dcc7b657e78891284", - "skills/base44-platform-workflows/references/automations-legacy.md": "df52bfdc18a3e7b886c169224edb68479771efd2f71eebd2652f43482caf10bc", - "skills/base44-platform-workflows/references/mobile-automations.md": "7ab2c859ac0ad6f531ae0918a7bf125e5611af76e7905693c0f78c8e2c83799e", - "skills/base44-platform-workflows/references/web-spa.md": "ba6b6f4165cfbb9ce7297ed7568ac515112038f5a2eae38f3ab29b5a4557aed0", - "skills/base44-platform-workflows/references/web-tanstack.md": "a91a7cb530031380439ca2a8779069f70458efc32082c58fd01e54e3aaf2bb3c" + "skills/base44-platform-actors/SKILL.md": "c3844ddcf866e7c4f1adb922a7fec64c0fd44427d9356f4ea104b08f031a9f13", + "skills/base44-platform-actors/references/mobile.md": "ed1071e7cb1af1169db8fcca1758cf03285ef7e1d345c8d601486a2782b0a5b6", + "skills/base44-platform-actors/references/web.md": "9114381a0228fbfab490679ecbf5b0d5129993a8e901d51175cfe31124a68c85", + "skills/base44-platform-agents/SKILL.md": "c791b870ff47eeafedcf2604e42b57b823043856d23d1cf657fb2198333cdb5f", + "skills/base44-platform-agents/references/web-spa.md": "a99c7b5b6c93b0033d253574b5a94e7b8871953cd26d6c24c5c2bde68781d990", + "skills/base44-platform-agents/references/web-tanstack.md": "6c8b9fc5af17b71b516902febca8deca1a66ca334ef6678da0170998ebee426c", + "skills/base44-platform-backend-functions/SKILL.md": "0b1034eb6965a723fd10b5b70fa4616e2297938087a019fc8fce30d10e27f794", + "skills/base44-platform-backend-functions/references/mobile.md": "0163f74762ed3ea0b19f81d7f635e6dc74c0902c3b3456f1187883ea18f91512", + "skills/base44-platform-backend-functions/references/web-spa.md": "3806589571b0a3d4c6a665f0672ab39ff4ea06c6c10b996a30c2991d593df2f6", + "skills/base44-platform-backend-functions/references/web-tanstack.md": "ad209a49e50b5668b479b98f13ec903da5554d9b540e3b10c12d4851898309c6", + "skills/base44-platform-connectors/SKILL.md": "278579146b5e422f11cb15a9b33e5955cd9cbb62f0791c4cd40ca49535492a51", + "skills/base44-platform-connectors/references/mobile.md": "cc30ab45866ebb4952fd8ec45954a756d7b82985dc12704cd8ff3118d330afb3", + "skills/base44-platform-connectors/references/web-spa.md": "7ef4ae7afc1c5b00d55fdfc1330fefad226411c0065c0d82a263e9a198aae482", + "skills/base44-platform-connectors/references/web-tanstack.md": "64ddd48bc69e877038d677ea666fecab6c37d8103f1f1acea61a00ff6111e1f8", + "skills/base44-platform-entities/SKILL.md": "5e40879dbb791b271953e8decc5b80ed63e950668b9522b65b45eb9c3647ad62", + "skills/base44-platform-entities/references/mobile-and-games.md": "180accec4af5547da524e09fed6ed36414ea3d64f24cf622e40654a51565bb8a", + "skills/base44-platform-entities/references/rls.md": "a87a71751bb63c28a0629c2ca65019c726c3b549149a029f74f5a84cb86b8dcf", + "skills/base44-platform-entities/references/web.md": "237aab59548b57ac6d20ef8ca6efeb0c50e42c72f16158fd6711e104c090a031", + "skills/base44-platform-frontend/SKILL.md": "cfb5241785f6f0b6cba3cf1c6206849e19e20a9f46d1851a37afe840cbd90e59", + "skills/base44-platform-frontend/references/mobile.md": "ecf9499821d8cf345e8b32143f2d0f3e72a4daeda06588a251d26f546bcac60a", + "skills/base44-platform-frontend/references/web-spa.md": "410aee9583f1cf24dead07d4429d27ca7063e38053d50aa1cf7bc2a88305c56b", + "skills/base44-platform-frontend/references/web-tanstack.md": "5c4227b1fc2fa80f2da40dff60e68f67f8c0054cdbf7758bc43bb6cea49d6614", + "skills/base44-platform-games/SKILL.md": "4338b805e5e066045e015d26d5c4bb9aacfb077aa828095f723608b11662abbf", + "skills/base44-platform-games/references/game-2d.md": "242033f3bfff4e45630d68c232d9a576d6a74d3a794001413937398af734a13b", + "skills/base44-platform-games/references/game-3d.md": "782a89cb6cab7355b5e2016cb401e0f0966082853b5a96fb5ff876807d5eee39", + "skills/base44-platform-games/references/phaser/actions_and_utilities.md": "66e7414bbb95280f91bd36dd933a578bbf6dfe8e6de284a17de774bd3da1c759", + "skills/base44-platform-games/references/phaser/animations.md": "4ad18783e90621a36e4dc059cbed212a5e8159a1b09b41db8b157a34e481966f", + "skills/base44-platform-games/references/phaser/audio_and_sound.md": "c3d8d1b1a0e201bbd5e181159755e7c97355694fe1dfcf566687cd660c7ab72c", + "skills/base44-platform-games/references/phaser/cameras.md": "e118aecc47ab3117187b2e66aea784a37ad1031a9ed46f23bfbbeb71e4fa4542", + "skills/base44-platform-games/references/phaser/curves_and_paths.md": "057362007b900aa9afed476821cd5d33336810d08f891b1403ad3ef0f004490c", + "skills/base44-platform-games/references/phaser/data_manager.md": "cbc11427efa8e8165638c23a09e62b4e2df0d106b41f9e1c8b5ee79a34ee7784", + "skills/base44-platform-games/references/phaser/events_system.md": "8ed1067d88b7a3076bcee9b6d9fbf38e5b8993b0ee02f7110c92fe03050049bf", + "skills/base44-platform-games/references/phaser/filters_and_postfx.md": "0f99a61b7ea509bf98bf44019ee03977bc98b93feb196fb935cd00b84467261e", + "skills/base44-platform-games/references/phaser/game_object_components.md": "f339f7314e2aeb2d5f044123c34f972e4101b31e83bdd06b3089ceefaaf0112e", + "skills/base44-platform-games/references/phaser/game_setup_and_config.md": "5dcf957a43ac39c380f41eea628ae9e57c659a418fb38c1535723d99b8f8d354", + "skills/base44-platform-games/references/phaser/geometry_and_math.md": "1ebbe11154252933a2744c47366c3c1f08a4bf57589ad57cb666f4448977f74d", + "skills/base44-platform-games/references/phaser/graphics_and_shapes.md": "5149f3dfc8a22c8be01f1564e913e54dacb56a460e66d4cc89d386a71c0bbcd3", + "skills/base44-platform-games/references/phaser/groups_and_containers.md": "61e1d27f415612bf3475e0bf8f49f2bb87b9ae22ad67eedf80c0f9814d38cc21", + "skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md": "ff3feb37602af540a4626dcdd12aebabe6679aa4ce629715aa4d4bcdb95088f3", + "skills/base44-platform-games/references/phaser/loading_assets.md": "c1780741e20f125e322cdb547c2ed05425d64d3ca2cefdd44bd8b6789cc24f16", + "skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md": "7522787a0513f6db0dcdb03e113ca85898a0dec694f33e82abce998aa65fe8e1", + "skills/base44-platform-games/references/phaser/particles.md": "7004c2c67f88a8cb1719b3e24abfff03a0550527be24ad119256fe2d206c14cf", + "skills/base44-platform-games/references/phaser/physics_arcade.md": "73efe5bf13739dc759a3c277c3064716cb2224740b4f0b8c2c036b88eceb0862", + "skills/base44-platform-games/references/phaser/physics_matter.md": "620b1a18794e1650d5b2580ea212caddc379da2665549b05da18fe9e9242236c", + "skills/base44-platform-games/references/phaser/render_textures.md": "d7422316735a8cedde9712850ba13e6a90369aa20d144fe0531872e261925948", + "skills/base44-platform-games/references/phaser/scale_and_responsive.md": "1ff70ea1cf861329a71f70bc913790dae332cb2e47d83ade2e3e08e3def54c9e", + "skills/base44-platform-games/references/phaser/scenes.md": "09ef9a0a4f91955f86defc5b22fbf7cfd77ae01797c035a345ab85d72719fdca", + "skills/base44-platform-games/references/phaser/sprites_and_images.md": "9f063ee60fae8ae057c1034c8503e9ed813f82e91c9e296dec2245e0670dc5a0", + "skills/base44-platform-games/references/phaser/text_and_bitmaptext.md": "99fc0b2503bc6a6ed55193ee9a579143a352fc2b32be9556eef6a54a4974b206", + "skills/base44-platform-games/references/phaser/tilemaps.md": "ef5f623a332585fe19568b342fbf02e2e5722d1042705945add1dc5b933c447e", + "skills/base44-platform-games/references/phaser/time_and_timers.md": "ebcce54b7de4586a30d4055ed67ccbf5cffc28061db9afc32c5c8c4c18c315a7", + "skills/base44-platform-games/references/phaser/tweens.md": "c5a7062d488039081ed7b24013fa13e78dea24125c7b972cfcb296436db6d0c5", + "skills/base44-platform-games/references/threejs/assets_models_textures.md": "e011402834044d6e9205df7656726baceaa6aa98d9ade20f586d5ddce9236f29", + "skills/base44-platform-games/references/threejs/cameras.md": "31b598dd5c6b8fa6e40298a26e72fe5260fd1d9020dd768314a12369f8955bea", + "skills/base44-platform-games/references/threejs/character_controller.md": "626b77a397f0362f08b183b2f8bd627ed513a43a7cb8f74b3b993def3f652c0f", + "skills/base44-platform-games/references/threejs/enemies_and_movement.md": "e43c30fb7bed6d33749d483a1aa563ec3b205dcf76ad22d083c9f4c6eaa81dac", + "skills/base44-platform-games/references/threejs/game_state_and_flow.md": "bdfc7a33b0dc979a9d1b39c6935e04ee81485773b1d201883036ac5e571a17cf", + "skills/base44-platform-games/references/threejs/input_and_controls.md": "56bd480ea000ad359e644d3b1811bbefe16a0843a4ae4d947ee06fc1c9c6e494", + "skills/base44-platform-games/references/threejs/performance.md": "ed57ede1db9439ee663e71c6d4b2e579c8956c960fe69af79c5e8e0f57a11e4d", + "skills/base44-platform-games/references/threejs/physics_dynamics.md": "75f0017a31867a4fb6a975393c099143ff57e1e031ad277af04c88662f73241c", + "skills/base44-platform-games/references/threejs/projectiles_and_pooling.md": "b2137f885ee8fd63868dae1584676a2e34c0365e1bcc31b60b1ed0b8845352dd", + "skills/base44-platform-games/references/threejs/scene_and_rendering.md": "667ea68cda6b354ffb6d54a7ce5050a661e8a997d3255f1c664440b00bd0cc30", + "skills/base44-platform-games/references/threejs/text_hud_ui.md": "c9bef2170523bf1c679f1608730004fe9c6e9b02254cc1be2102113983d09658", + "skills/base44-platform-games/references/threejs/triggers_and_collisions.md": "f981ba3c28e7f65f11e9f01e082ba24490ddb7e0c5aa9ee961d008a421f47758", + "skills/base44-platform-games/references/threejs/vfx_and_juice.md": "bada426d6e940194b5d476ac2958940bc7a012e7743ea7a6f7bdd702ca35006b", + "skills/base44-platform-integrations-sdk/SKILL.md": "eea0a0061264ed71408db5b38787556d892472a13f2995d8c70464942bc143c2", + "skills/base44-platform-integrations-sdk/references/mobile.md": "824d6a81dcfb5eff3dd51c48debf3324b5db805db21f44884b75e834abe1a599", + "skills/base44-platform-integrations-sdk/references/web-spa.md": "09d9fe218764e0f116dc725fb76cd54559dc5fdd34aa3cc0d14663757343d423", + "skills/base44-platform-integrations-sdk/references/web-tanstack.md": "bd1d6422c866290b2b2dc095bcd39e571f87c691d6150eac7bdb92cc15d4c6da", + "skills/base44-platform-platform/SKILL.md": "a3d6c63e17a6227315c8351cd7756db6612ad5e7a26aee844773073ec68ffcaf", + "skills/base44-platform-platform/references/game.md": "5693f5d4c492fa669d951e8f2e85e713cf100009f8db9a5a0aa1842a74e5efba", + "skills/base44-platform-platform/references/mobile.md": "bc5116e7e7896f56908dd8e400cb7b7101f0ee53597725a0af614002b1ac669a", + "skills/base44-platform-platform/references/web-spa.md": "52917f4b90d778d21e73b70ab7862e9250044831b51db4dd8723cf68a6706941", + "skills/base44-platform-platform/references/web-tanstack.md": "625c2c6f75647c843292b9a864db8b737b674848cae1d9e44e9630185c8429d9", + "skills/base44-platform-workflows/SKILL.md": "c48b355ef3298ac81c662c4bb5bffb40486ede2ea42405d8ff00d3a01d46fae9", + "skills/base44-platform-workflows/references/automations-legacy.md": "8646424e87b879805c917726bbdb8c025ae66a1353e1d660bb9af6df5e561202", + "skills/base44-platform-workflows/references/mobile-automations.md": "80f00126db3869fa60919c4fac18cb6d8bcaa93019338a75e7a41e78a4f0249a", + "skills/base44-platform-workflows/references/web-spa.md": "f3f7f30a1ac50c60ef47c94526c662587aa6516bc639560de55e3cde71af62cc", + "skills/base44-platform-workflows/references/web-tanstack.md": "8102264e570c95f712c20f73f8d724b044ff33a5a0953a918c9a944ba5b36445" } } diff --git a/skills/base44-platform-platform/SKILL.md b/skills/base44-platform-platform/SKILL.md index c66594a..b758144 100644 --- a/skills/base44-platform-platform/SKILL.md +++ b/skills/base44-platform-platform/SKILL.md @@ -2,9 +2,9 @@ name: base44-platform-platform description: "Base44 platform concepts for a web app: entities, backend functions or server code, workflows, pages and layouts, the project file structure, authentication (platform login vs the custom-auth boilerplate), user invites and the GitHub features. Read first when starting work on a Base44 web app." --- -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 platform A Base44 web app is a Vite + React SPA or a TanStack Start project backed by Base44's entities, functions and auth. This reference is the platform orientation the builder itself works from: concepts, file layout, authentication wiring and user invites. diff --git a/skills/base44-platform-platform/references/game.md b/skills/base44-platform-platform/references/game.md index 1be918a..f6fd9c5 100644 --- a/skills/base44-platform-platform/references/game.md +++ b/skills/base44-platform-platform/references/game.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 platform: Game (Phaser 2D or Three.js 3D) <technology_stack> diff --git a/skills/base44-platform-platform/references/mobile.md b/skills/base44-platform-platform/references/mobile.md index 6da08ff..6a87190 100644 --- a/skills/base44-platform-platform/references/mobile.md +++ b/skills/base44-platform-platform/references/mobile.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 platform: Native mobile app (Expo) Here's a list of concepts you can use to develop the app: diff --git a/skills/base44-platform-platform/references/web-spa.md b/skills/base44-platform-platform/references/web-spa.md index cbdd316..acf719c 100644 --- a/skills/base44-platform-platform/references/web-spa.md +++ b/skills/base44-platform-platform/references/web-spa.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 platform: Web app, Vite SPA <concepts> diff --git a/skills/base44-platform-platform/references/web-tanstack.md b/skills/base44-platform-platform/references/web-tanstack.md index 04e0f06..9bda2b8 100644 --- a/skills/base44-platform-platform/references/web-tanstack.md +++ b/skills/base44-platform-platform/references/web-tanstack.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 platform: Web app, TanStack Start <concepts> diff --git a/skills/base44-platform-workflows/SKILL.md b/skills/base44-platform-workflows/SKILL.md index 6e3851e..d10d08f 100644 --- a/skills/base44-platform-workflows/SKILL.md +++ b/skills/base44-platform-workflows/SKILL.md @@ -2,9 +2,9 @@ name: base44-platform-workflows description: "Base44 workflows: trigger-driven automated processes defined as CNCF Serverless Workflow files under base44/workflows/ (scheduled, entity, connector, in-app agent, auth, publish and payment triggers; call, wait and switch steps; jq expressions), with the full authoring guide, plus the legacy automations pipeline for apps that predate workflows. Use for any 'when X happens, do Y' or 'every N hours, do Y'." --- -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 workflows Workflows is the default pipeline: a `.jsonc` envelope under `base44/workflows/` is live once written. An app whose profile says `automation_pipeline: legacy_automations` uses the automations reference instead. diff --git a/skills/base44-platform-workflows/references/automations-legacy.md b/skills/base44-platform-workflows/references/automations-legacy.md index e875357..d9fa4f6 100644 --- a/skills/base44-platform-workflows/references/automations-legacy.md +++ b/skills/base44-platform-workflows/references/automations-legacy.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 workflows: Legacy automations (apps that predate workflows) <automations_capability> diff --git a/skills/base44-platform-workflows/references/mobile-automations.md b/skills/base44-platform-workflows/references/mobile-automations.md index a862d39..d42b064 100644 --- a/skills/base44-platform-workflows/references/mobile-automations.md +++ b/skills/base44-platform-workflows/references/mobile-automations.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 workflows: Automations (mobile apps) <automations_capability> diff --git a/skills/base44-platform-workflows/references/web-spa.md b/skills/base44-platform-workflows/references/web-spa.md index 050338d..ff26201 100644 --- a/skills/base44-platform-workflows/references/web-spa.md +++ b/skills/base44-platform-workflows/references/web-spa.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 workflows: Workflows (Vite SPA: steps call backend functions) <workflows_capability> diff --git a/skills/base44-platform-workflows/references/web-tanstack.md b/skills/base44-platform-workflows/references/web-tanstack.md index 36000d5..670e287 100644 --- a/skills/base44-platform-workflows/references/web-tanstack.md +++ b/skills/base44-platform-workflows/references/web-tanstack.md @@ -1,6 +1,6 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled by apper `backend/scripts/agent_skills` from the - builder prompt sources; the apper commit is in /APPER_COMMIT at the repository root. - Edits here are overwritten by the nightly sync; change the source partials in apper. --> +<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the + source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by + the next sync; change the source instead. --> # Base44 workflows: Workflows (TanStack Start: steps call server routes) <workflows_capability> From 1216826d95918aea9385441aa0f6a3700c23299c Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Thu, 22 Oct 2026 10:56:19 +0000 Subject: [PATCH 07/15] sync: recompile after the first IP review (source 9fc7fc6ccc0) Internal-only passages are now cut at the source (dispatcher route and auth, actor key seeding), builder-only mechanics are rephrased for external agents (Phaser reference files instead of injected sections, npm install, the Dashboard's Webhooks page), and the two social-post activities that need a builder-only tool leave the workflows guide and index. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLKGecSgMqR14kYQTrugoR --- SOURCE_COMMIT | 2 +- .../references/mobile.md | 4 +- .../base44-platform-actors/references/web.md | 4 +- .../references/web-spa.md | 3 +- .../references/web-tanstack.md | 3 +- .../references/mobile.md | 4 +- .../references/web-spa.md | 6 +- .../references/web.md | 25 ++---- .../references/game-2d.md | 6 +- .../threejs/character_controller.md | 2 +- .../references/threejs/game_state_and_flow.md | 2 +- .../references/threejs/vfx_and_juice.md | 2 +- skills/base44-platform-manifest.json | 30 +++---- .../references/automations-legacy.md | 2 +- .../references/mobile-automations.md | 2 +- .../references/web-spa.md | 86 ++----------------- .../references/web-tanstack.md | 86 ++----------------- 17 files changed, 58 insertions(+), 211 deletions(-) diff --git a/SOURCE_COMMIT b/SOURCE_COMMIT index ca72e8a..059c257 100644 --- a/SOURCE_COMMIT +++ b/SOURCE_COMMIT @@ -1 +1 @@ -7acabe324ef52de6aaf6a937b56eb5ac74d64a76 +9fc7fc6ccc00148c3a2cf18206a3653108ac7fb0 diff --git a/skills/base44-platform-actors/references/mobile.md b/skills/base44-platform-actors/references/mobile.md index 6d166d7..bd2c6de 100644 --- a/skills/base44-platform-actors/references/mobile.md +++ b/skills/base44-platform-actors/references/mobile.md @@ -92,8 +92,8 @@ export default class BoardRoom extends Actor { - `this.broadcast(...)` is for ROOM-wide state; events about ONE client (your assigned color, your validation error, your private draft) go `conn.send(...)` — broadcasting them leaks private state and makes every client react. Messages are JSON both ways; `type` values starting with `__` are platform-reserved. - `this.client` is a ready-made `@base44/sdk` client on every actor (`await this.client.functions.invoke('name', input)` — there is no `functions.name()` form, and the returned value sits on `.data`; `await this.client.entities.Thing.filter(...)`) acting as the app's ANONYMOUS role, pinned to the actor's own deploy. `this.client.asServiceRole` (`entities`, `functions`, `integrations`) is the admin-level client, as in a backend function: use it for work the ROOM owns and has validated — persisting its durable result, maintaining its registry row, reading private config — never to do what a client asked without validating it, and never loosen RLS to the anonymous role instead. Neither client is a signed-in user: attribute service-role records with an explicit field from `conn.identity.userId` (authenticated identities only). The first privileged call after a wake pays a token exchange — keep it on persist paths, not per message. - Durable results (the finished drawing, a chat transcript, an exported document): the actor broadcasts the authoritative result, writes it to `this.storage`, and persists it to entities via `asServiceRole`, keyed by the room instance id and IDEMPOTENT (check for the existing record before creating; readers treat the earliest record per key as canonical) — delivery and wakes are at-least-once. Re-`conn.send` it to (re)connecting clients: shells cannot read actor storage. The shell writes only records the USER owns, never the room's canonical result. -- Actor auth is automatic: the first actor deploy generates an `ACTOR_TOKEN_SECRET` app secret (one per app, shared by all actors) that seeds the connection-token keys. Never ask the user for it or set it yourself; overwriting it rotates the keys and redeploys the actors; it is not readable from function or actor env. -- Rooms and discovery: one room = one session (a drawing board, a chat channel, a document) — never funnel everyone into a single global room. For browsable rooms keep a registry ENTITY (e.g. Room: status, user_count) whose record id IS the actor instance id; the registry is advertising, the actor is truth — admission is decided in `handleConnect`, never from the row. List rooms by subscribing to the entity FIRST, then fetching and reconciling by id; filter to recently-updated rows (crashed rooms leave stale ones). PRIVATE rooms: there is no room-level auth yet — the instance id is the only admission control, so mint it with `crypto.randomUUID()` (record ids are enumerable ObjectIds), never list it in a readable registry, and share it only as an invite link/code. +- Actor auth is automatic. Never ask the user for `ACTOR_TOKEN_SECRET` or set it yourself; it is not readable from function or actor env. +- Rooms and discovery: one room = one session (a drawing board, a chat channel, a document) — never funnel everyone into a single global room. For browsable rooms keep a registry ENTITY (e.g. Room: status, user_count) whose record id IS the actor instance id; the registry is advertising, the actor is truth — admission is decided in `handleConnect`, never from the row. List rooms by subscribing to the entity FIRST, then fetching and reconciling by id; filter to recently-updated rows (crashed rooms leave stale ones). PRIVATE rooms: there is no room-level auth — the instance id is the only admission control, so mint it with `crypto.randomUUID()` (record ids are guessable), never list it in a readable registry, and share it only as an invite link/code. </actor_rules> <actor_client> diff --git a/skills/base44-platform-actors/references/web.md b/skills/base44-platform-actors/references/web.md index 7c92138..6f82805 100644 --- a/skills/base44-platform-actors/references/web.md +++ b/skills/base44-platform-actors/references/web.md @@ -92,8 +92,8 @@ export default class BoardRoom extends Actor { - `this.broadcast(...)` is for ROOM-wide state; events about ONE client (your assigned color, your validation error, your private draft) go `conn.send(...)` — broadcasting them leaks private state and makes every client react. Messages are JSON both ways; `type` values starting with `__` are platform-reserved. - `this.client` is a ready-made `@base44/sdk` client on every actor (`await this.client.functions.invoke('name', input)` — there is no `functions.name()` form, and the returned value sits on `.data`; `await this.client.entities.Thing.filter(...)`) acting as the app's ANONYMOUS role, pinned to the actor's own deploy. `this.client.asServiceRole` (`entities`, `functions`, `integrations`) is the admin-level client, as in a backend function: use it for work the ROOM owns and has validated — persisting its durable result, maintaining its registry row, reading private config — never to do what a client asked without validating it, and never loosen RLS to the anonymous role instead. Neither client is a signed-in user: attribute service-role records with an explicit field from `conn.identity.userId` (authenticated identities only). The first privileged call after a wake pays a token exchange — keep it on persist paths, not per message. - Durable results (the finished drawing, a chat transcript, an exported document): the actor broadcasts the authoritative result, writes it to `this.storage`, and persists it to entities via `asServiceRole`, keyed by the room instance id and IDEMPOTENT (check for the existing record before creating; readers treat the earliest record per key as canonical) — delivery and wakes are at-least-once. Re-`conn.send` it to (re)connecting clients: shells cannot read actor storage. The shell writes only records the USER owns, never the room's canonical result. -- Actor auth is automatic: the first actor deploy generates an `ACTOR_TOKEN_SECRET` app secret (one per app, shared by all actors) that seeds the connection-token keys. Never ask the user for it or set it yourself; overwriting it rotates the keys and redeploys the actors; it is not readable from function or actor env. -- Rooms and discovery: one room = one session (a drawing board, a chat channel, a document) — never funnel everyone into a single global room. For browsable rooms keep a registry ENTITY (e.g. Room: status, user_count) whose record id IS the actor instance id; the registry is advertising, the actor is truth — admission is decided in `handleConnect`, never from the row. List rooms by subscribing to the entity FIRST, then fetching and reconciling by id; filter to recently-updated rows (crashed rooms leave stale ones). PRIVATE rooms: there is no room-level auth yet — the instance id is the only admission control, so mint it with `crypto.randomUUID()` (record ids are enumerable ObjectIds), never list it in a readable registry, and share it only as an invite link/code. +- Actor auth is automatic. Never ask the user for `ACTOR_TOKEN_SECRET` or set it yourself; it is not readable from function or actor env. +- Rooms and discovery: one room = one session (a drawing board, a chat channel, a document) — never funnel everyone into a single global room. For browsable rooms keep a registry ENTITY (e.g. Room: status, user_count) whose record id IS the actor instance id; the registry is advertising, the actor is truth — admission is decided in `handleConnect`, never from the row. List rooms by subscribing to the entity FIRST, then fetching and reconciling by id; filter to recently-updated rows (crashed rooms leave stale ones). PRIVATE rooms: there is no room-level auth — the instance id is the only admission control, so mint it with `crypto.randomUUID()` (record ids are guessable), never list it in a readable registry, and share it only as an invite link/code. </actor_rules> <actor_client> diff --git a/skills/base44-platform-agents/references/web-spa.md b/skills/base44-platform-agents/references/web-spa.md index 450f02e..44715a4 100644 --- a/skills/base44-platform-agents/references/web-spa.md +++ b/skills/base44-platform-agents/references/web-spa.md @@ -39,7 +39,7 @@ Entity entries take `entity_name` + `allowed_operations`; backend-function entri </tools> <channels> -Agents support WhatsApp and Telegram — add a channel only when explicitly requested. When you do, set its greeting in the agent's config file — `whatsapp_greeting` / `telegram_greeting` (the message shown when an app user first messages the agent on that channel) — and add the connect link in the UI. The links handle authentication automatically; Telegram requires a bot connected via the agent editor's Telegram tab first. +Agents support WhatsApp, Telegram and LINE — add a channel only when explicitly requested. When you do, set its greeting in the agent's config file — `whatsapp_greeting` / `telegram_greeting` / `line_greeting` (the message shown when an app user first messages the agent on that channel) — and, for WhatsApp and Telegram, add the connect link in the UI. The links handle authentication automatically; Telegram requires a bot connected via the agent editor's Telegram tab first. LINE has no app connect link: its activation codes come only from the agent editor's LINE tab (shown only in some regions) for app editors and expire in 30 minutes, so an in-app LINE button cannot connect app users. When asked for one, say so instead of building a button that cannot work. ```jsx import { base44 } from "@/api/base44Client"; @@ -60,7 +60,6 @@ import { base44 } from "@/api/base44Client"; const conversation = base44.agents.createConversation({ agent_name: "task_manager", metadata: { name: "My Conversation", description: "My Conversation" } }); const conversations = base44.agents.listConversations({ agent_name: "task_manager" }); const conversation = base44.agents.getConversation(conversationId); // conversation.messages = full history, for loading into the UI -base44.agents.updateConversation(conversationId, { metadata: { name: "...", description: "..." } }); base44.agents.addMessage(conversation, { role: "user", content: "Hello, how are you?" }); // pass the entire conversation object base44.agents.addMessage(conversation, { role: "user", content: "Hello", file_urls: ["...url1"] }); // upload files first, then pass URLs diff --git a/skills/base44-platform-agents/references/web-tanstack.md b/skills/base44-platform-agents/references/web-tanstack.md index 5ea2cf8..e137a1a 100644 --- a/skills/base44-platform-agents/references/web-tanstack.md +++ b/skills/base44-platform-agents/references/web-tanstack.md @@ -41,7 +41,7 @@ A server-route tool calls one of this app's server routes (`src/routes/api/…`) </tools> <channels> -Agents support WhatsApp and Telegram — add a channel only when explicitly requested. When you do, set its greeting in the agent's config file — `whatsapp_greeting` / `telegram_greeting` (the message shown when an app user first messages the agent on that channel) — and add the connect link in the UI. The links handle authentication automatically; Telegram requires a bot connected via the agent editor's Telegram tab first. +Agents support WhatsApp, Telegram and LINE — add a channel only when explicitly requested. When you do, set its greeting in the agent's config file — `whatsapp_greeting` / `telegram_greeting` / `line_greeting` (the message shown when an app user first messages the agent on that channel) — and, for WhatsApp and Telegram, add the connect link in the UI. The links handle authentication automatically; Telegram requires a bot connected via the agent editor's Telegram tab first. LINE has no app connect link: its activation codes come only from the agent editor's LINE tab (shown only in some regions) for app editors and expire in 30 minutes, so an in-app LINE button cannot connect app users. When asked for one, say so instead of building a button that cannot work. ```jsx import { base44 } from "@/api/base44Client"; @@ -62,7 +62,6 @@ import { base44 } from "@/api/base44Client"; const conversation = base44.agents.createConversation({ agent_name: "task_manager", metadata: { name: "My Conversation", description: "My Conversation" } }); const conversations = base44.agents.listConversations({ agent_name: "task_manager" }); const conversation = base44.agents.getConversation(conversationId); // conversation.messages = full history, for loading into the UI -base44.agents.updateConversation(conversationId, { metadata: { name: "...", description: "..." } }); base44.agents.addMessage(conversation, { role: "user", content: "Hello, how are you?" }); // pass the entire conversation object base44.agents.addMessage(conversation, { role: "user", content: "Hello", file_urls: ["...url1"] }); // upload files first, then pass URLs diff --git a/skills/base44-platform-backend-functions/references/mobile.md b/skills/base44-platform-backend-functions/references/mobile.md index 116504e..adbe161 100644 --- a/skills/base44-platform-backend-functions/references/mobile.md +++ b/skills/base44-platform-backend-functions/references/mobile.md @@ -9,7 +9,7 @@ It has to export a fetch function that will be called with the incoming request. The Core-integration routing rule is an exception: use a backend function for Core methods except `UploadPublicFile`, `UploadPrivateFile` and `CreateFileSignedUrl` (`UploadFile` is the retired spelling of `UploadPublicFile`). -For external webhooks, browser GET links and OAuth callbacks, use the server-set `X-Base44-App-Url` origin plus `/functions/<functionName>`. Never expose dispatcher `/run/<scriptId>` or deployment URLs; the dispatcher requires platform-only authentication. Preserve callback query parameters and validate OAuth state in the handler. +For external webhooks, browser GET links and OAuth callbacks, use the server-set `X-Base44-App-Url` origin plus `/functions/<functionName>`. Never expose deployment URLs. Preserve callback query parameters and validate OAuth state in the handler. <code_validation> **Validation Flow**: After you generate the function code, it automatically goes through lint validation before deployment. @@ -99,7 +99,7 @@ The response is an Axios response object with structure: `{data, status, headers 3. Pin a version on every external import. For the Base44 SDK use exactly `npm:@base44/sdk@0.8.54`. 4. Already have an `esm.sh` or `unpkg.com` URL? Replace the CDN hostname with the `npm:` specifier. 5. Export a default request handler: `export default async function(req) { ... }`. -6. Secrets: `import { secrets } from "base44:runtime"` → `secrets.get("NAME")`; pre-populated: BASE44_APP_ID. Set other secrets in dashboard environment variables. +6. Secrets: `import { secrets } from "base44:runtime"` → `secrets.get("NAME")`; pre-populated: BASE44_APP_ID. Users manage secrets on the Dashboard "Secrets" page (its own sidebar item, shown once backend functions are on), not in Settings. 7. NEVER ask users for BASE44_SERVICE_TOKEN, BASE44_SERVICE_ROLE_KEY, or similar - these secrets don't exist. Use `createClientFromRequest(req)` for auth, and `base44.asServiceRole` for service role level operations. 8. Naming: USE ONLY camelCase for function names. No spaces, no /, no hyphens. **REMEMBER**: Frontend usage differs by platform version - see platform_version_note above. diff --git a/skills/base44-platform-backend-functions/references/web-spa.md b/skills/base44-platform-backend-functions/references/web-spa.md index fbe7b1b..c1a7e0c 100644 --- a/skills/base44-platform-backend-functions/references/web-spa.md +++ b/skills/base44-platform-backend-functions/references/web-spa.md @@ -18,8 +18,8 @@ For external-service integrations, in order: </integration_priority> <secrets> -Before writing a function that needs a secret/API key, check it's set — using an unset secret fails. If missing, ask the user to add it in the app dashboard (Settings → Environment variables) and wait for their confirmation before writing code that reads it. -Secrets: `import { secrets } from "base44:runtime"` → `secrets.get("NAME")`; pre-populated: BASE44_APP_ID. Set other secrets in dashboard settings - environment variables. +Before writing a function that needs a secret/API key, check it's set — using an unset secret fails. If missing, ask the user to add it on the app Dashboard's "Secrets" page (its own sidebar item, shown once backend functions are on) and wait for their confirmation before writing code that reads it. +Secrets: `import { secrets } from "base44:runtime"` → `secrets.get("NAME")`; pre-populated: BASE44_APP_ID. Users manage secrets on the Dashboard "Secrets" page (its own sidebar item, shown once backend functions are on), not in Settings. Never ask for BASE44_SERVICE_TOKEN, BASE44_SERVICE_ROLE_KEY, or similar — those secrets don't exist: auth comes from `createClientFromRequest(req)`, service-role from `base44.asServiceRole`. </secrets> @@ -42,7 +42,7 @@ Requirements: </structure_and_validation> <coding_guidelines> -For external webhooks, browser GET links and OAuth callbacks, use the server-set `X-Base44-App-Url` origin plus `/functions/<functionName>`. Never expose dispatcher `/run/<scriptId>` or deployment URLs; the dispatcher requires platform-only authentication. Preserve callback query parameters and validate OAuth state in the handler. +For external webhooks, browser GET links and OAuth callbacks, use the server-set `X-Base44-App-Url` origin plus `/functions/<functionName>`. Never expose deployment URLs. Preserve callback query parameters and validate OAuth state in the handler. - Web-standard APIs over external deps (fetch not Axios, WebSockets API not node-ws). - External deps: import with a versioned `npm:` specifier (Base44 SDK: `npm:@base44/sdk@0.8.54`); rewrite `esm.sh` / `unpkg.com` URLs to `npm:`. - JavaScript, not TypeScript. Names camelCase only: no spaces, `/`, hyphens. diff --git a/skills/base44-platform-entities/references/web.md b/skills/base44-platform-entities/references/web.md index 2252017..ffd99fb 100644 --- a/skills/base44-platform-entities/references/web.md +++ b/skills/base44-platform-entities/references/web.md @@ -17,27 +17,16 @@ Never store large content (base64, PDFs, blobs) in entity fields — upload via </field_limits> <entities_sdk> -Entities SDK on the pre-initialized base44 client; each entity is base44.entities.EntityName. Calls are rate-limited per app and every record read counts against the app's budget, so the server does the filtering, counting and math. Pick the call by what the screen needs: -- a list of records → filter(query, {sort, limit, fields}), a page: read .items, never treat it as an array; more rows on demand → the same call with cursor -- a number (total, badge, "N of M") → count(query); a sum or average → aggregate -- totals per group, a chart, a leaderboard, "top products" → aggregate({groupBy or dateBucket, sum/avg/min/max}) -- dropdown or autocomplete values → the schema enum when the field declares one, else filter(query, {distinct: 'field'}) -- import or sync keyed by your own id → upsert(records, {key}) -- every record → only for an export (CSV download, backup, sync), with the cursor loop -A list on screen is one page: filter(query, {sort, limit: 50}) with a "load more" that passes cursor, not a 500-row fetch into state. A number or grouping added to an existing page gets its own count/aggregate call even when a list is already loaded — that list is one page, not the data — and replacing a loaded-list computation with the server call is part of the ask, not a refactor. Every filter, dropdown choice or search term the user changes is a new filter() call with those conditions in the query (text search: $regex), replacing the page on screen; narrowing the loaded page in JS shows only what happened to be on it. Never load records to filter, count, sum or dedupe them in JS — `const all = await Sale.list(); all.filter(s => s.store === store).reduce(...)` is wrong even for a small table, because tables grow; `Sale.aggregate({query: {store}, sum: 'total'})` is right. A search box is a `$regex` filter on the server, not `.includes()` on a loaded list. +Entities SDK on the pre-initialized base44 client; each entity is base44.entities.EntityName. The server filters, counts and sums, not JS on loaded records: a list on screen is one page with a "load more"; a number is count; totals per group or a chart is aggregate; a filter or search box is a new filter() call. ```js import { base44 } from '@/api/base44Client'; -base44.entities.Todo.filter({priority: {$in: ['high', 'urgent']}, due_date: {$gte: '2026-01-01', $lt: '2026-02-01'}, archived: {$exists: false}}, {sort: '-due_date', limit: 100, fields: ['title', 'due_date']}) // → {items, next_cursor, has_more}: the options form ALWAYS returns a page object, never an array — read .items. MongoDB operators ($in/$nin/$gt/$gte/$lt/$lte/$ne/$exists/$or) on any field; fields returns only those (plus id) -base44.entities.Todo.filter({status: 'open'}, {sort: '-created_date', limit: 1000, cursor}) // one page: {items, next_cursor, has_more}. "Load more"/infinite scroll and export loops: first call without cursor, then cursor = page.next_cursor until has_more is false. Same options on list({sort, limit, cursor, fields}) -base44.entities.Todo.count({status: 'open'}) // a number, no records transferred: totals, badges, "N of M", pagination totals -// An entity with field-level read rules (per-field visibility in its schema) rejects aggregate and distinct with a 400: there, use count for numbers and one filter page for the rest, and say so in the summary -base44.entities.Todo.aggregate({query: {status: 'done'}, groupBy: 'owner', sum: ['amount', 'hours'], sort: '-sum_amount'}) // {rows: [{owner, count, sum_amount, sum_hours}], truncated}; one call computes every sum/avg/min/max you need; sort names a group field or a computed column (count, sum_<field>, avg_<field>…), nothing else. Also dateBucket: {field: 'created_date', unit: 'day'|'week'|'month'|'year'}, avg/min/max, countDistinct: 'field', having: {count: {$gt: 1}} (duplicates), limit up to 1000. Dashboards, totals per group, charts -// aggregate has the lowest rate limit (about 15 calls a minute per app): still far better than loading the collection to compute in JS, but spend it sparingly: several counters or totals on the same entity (per status, per owner, open vs done) come from one aggregate with groupBy, each read from its row, not one aggregate or count call per counter; a separate call only for another entity, another query, or a measure one call cannot hold (a second countDistinct) -base44.entities.Todo.list({distinct: 'category'}) // {items: values, next_cursor, has_more}: dropdowns and autocomplete; also filter(query, {distinct: 'field'}) -base44.entities.Todo.upsert(records, {key: 'external_id'}) // create-or-update by your own key (or {key: ['a', 'b']}), up to 500 per call: {created, updated, records}. Imports and syncs; never read first to check what exists -base44.entities.Todo.filter({name: {$regex: '^ann', $options: 'i'}}) // prefix/contains text match, only when the feature really needs it: it scans, so keep it behind another filter, keep the pattern simple, and never use it for exact matches +base44.entities.Todo.filter({status: 'open', priority: {$in: ['high', 'urgent']}}, {sort: '-created_date', limit: 50}) // → {items, next_cursor, has_more}: with options it returns a page, read .items. Load more: the same call with cursor: page.next_cursor. Operators: $in/$nin/$gt/$gte/$lt/$lte/$ne/$exists/$or, $regex for text search. Same options on list({sort, limit, cursor}) +base44.entities.Todo.count({status: 'open'}) // a number: totals, badges, "N of M" +base44.entities.Todo.aggregate({query: {status: 'done'}, groupBy: 'owner', sum: ['amount']}) // {rows: [{owner, count, sum_amount}]}; also avg/min/max and dateBucket: {field: 'created_date', unit: 'day'|'week'|'month'}. One call covers all counters of an entity +base44.entities.Todo.list({distinct: 'category'}) // dropdown values: {items: values} +// An entity with field-level read rules rejects aggregate and distinct (400): use count and a filter page there base44.entities.Todo.create({title: "Todo 1"}) base44.entities.Todo.bulkCreate([{title: "Todo 2"}, {title: "Todo 3"}]) base44.entities.Todo.update(todo.id, {description: "new"}) @@ -48,7 +37,7 @@ base44.entities.Todo.deleteMany({status: "archived"}) // deletes all matches in ``` <user_and_auth> -Built-in User entity uses the same SDK for reads (await base44.entities.User.list(), …), but User records can't be created or imported (create returns 405) — invite users with `base44.users.inviteUser(email, role)`. count, aggregate, upsert and the cursor form are not available on User. Auth SDK: +Built-in User entity uses the same SDK for reads (await base44.entities.User.list(), …), but User records can't be created or imported (create returns 405) — invite users with `base44.users.inviteUser(email, role)`. count, aggregate and the cursor form are not available on User. Auth SDK: - await base44.auth.me() — current user; can throw if not logged in on a public app. - await base44.auth.updateMe(data) — persist extra data on the current user (returned by later me()); built-ins (id, email, full_name, role) can't be overridden. - base44.auth.isAuthenticated() — Promise<boolean>. diff --git a/skills/base44-platform-games/references/game-2d.md b/skills/base44-platform-games/references/game-2d.md index 46b4e97..3238a2c 100644 --- a/skills/base44-platform-games/references/game-2d.md +++ b/skills/base44-platform-games/references/game-2d.md @@ -175,7 +175,7 @@ Do not mix inconsistent visual styles in the same scene unless the user explicit To opt-out, set `{"analytics":{"enabled":false}}` when initializing the base44 SDK (supported from 0.8.46). <phaser_reference> -Deep Phaser reference sections (physics, particles, tilemaps, etc.) are delivered inside user messages as <phaser_instructions> blocks when relevant. Sections from earlier messages in the conversation remain valid — rely on them even when the latest message carries none. +Deep Phaser reference sections (physics, particles, tilemaps, etc.) are the files under `references/phaser/`; read the one for the mechanic you are building before writing it. <scene_basics> Every Phaser scene must import Phaser or the Phaser classes it extends/uses. @@ -295,7 +295,7 @@ Browsers block autoplay. Start or resume audio after a user click/tap. </phaser_reference> <phaser_v4_guide> -This project uses Phaser 4 (the template ships `phaser@^4.1.0`). Several common Phaser 3 APIs were removed or changed in v4 — always use the v4 forms below, not the older v3 equivalents. These rules apply on EVERY turn; deeper per-topic detail arrives in the <phaser_instructions> sections of the user message when a topic is relevant. +This project uses Phaser 4 (the template ships `phaser@^4.1.0`). Several common Phaser 3 APIs were removed or changed in v4 — always use the v4 forms below, not the older v3 equivalents. These rules apply to every change; deeper per-topic detail is in the files under `references/phaser/`. <v3_apis_removed_use_v4> These Phaser 3 APIs are GONE in v4 — calling them throws or silently no-ops. Use the v4 form: @@ -317,7 +317,7 @@ When tempted to reach for a v3-era API not listed here, prefer the documented v4 </v3_apis_removed_use_v4> <v4_capabilities_to_use> -v4 adds GPU-accelerated systems worth reaching for. One-liners below; full usage rides the relevant <phaser_instructions> section when the topic comes up: +v4 adds GPU-accelerated systems worth reaching for. One-liners below; full usage is in the matching `references/phaser/` file: - Filters (`filters.internal` / `filters.external` after `enableFilters()`): modern post-processing on any object or camera — glow, blur, bloom, color matrix, displacement, vignette, masks. See filters_and_postfx. - Lighting + self-shadow (`setLighting(true)`, `setSelfShadow(...)`): cheap 2D lighting with no pipeline. See game_object_components. - New tint modes (`setTintMode(Phaser.TintModes.ADD | SCREEN | OVERLAY | HARD_LIGHT)`): additive glows and blends without filters. diff --git a/skills/base44-platform-games/references/threejs/character_controller.md b/skills/base44-platform-games/references/threejs/character_controller.md index 18758d0..1a0359a 100644 --- a/skills/base44-platform-games/references/threejs/character_controller.md +++ b/skills/base44-platform-games/references/threejs/character_controller.md @@ -18,7 +18,7 @@ A dynamic RigidBody with `colliders={false}` plus an explicit `<CapsuleCollider> </capsule_recipe> <ecctrl_fast_path> -For a full-featured third-person character (slopes, steps, moving platforms, camera follow modes) skip hand-rolling: install `ecctrl` with `install_npm_package` — the official pmndrs floating-capsule controller built on this exact stack. Wrap the character model in `<Ecctrl>` inside `<Physics>`; it expects the drei `KeyboardControls` action names `forward/backward/leftward/rightward/jump/run`, and its `<EcctrlJoystick />` (rendered OUTSIDE the Canvas, before it) adds mobile touch controls automatically. +For a full-featured third-person character (slopes, steps, moving platforms, camera follow modes) skip hand-rolling: install `ecctrl` with `npm install` — the official pmndrs floating-capsule controller built on this exact stack. Wrap the character model in `<Ecctrl>` inside `<Physics>`; it expects the drei `KeyboardControls` action names `forward/backward/leftward/rightward/jump/run`, and its `<EcctrlJoystick />` (rendered OUTSIDE the Canvas, before it) adds mobile touch controls automatically. Prefer ecctrl when the request is a real third-person game; prefer the capsule recipe for simple arcade movement or side-view platformers where its camera rig is unwanted. </ecctrl_fast_path> diff --git a/skills/base44-platform-games/references/threejs/game_state_and_flow.md b/skills/base44-platform-games/references/threejs/game_state_and_flow.md index 1ee51d1..8038957 100644 --- a/skills/base44-platform-games/references/threejs/game_state_and_flow.md +++ b/skills/base44-platform-games/references/threejs/game_state_and_flow.md @@ -10,7 +10,7 @@ Use this when structuring game phases (menu → playing → game over), score/he Two kinds of state, two homes: - Discrete/UI state (phase, score, lives, unlocks) — React state in the overlay, updated via EventBus events from gameplay. Low frequency, drives DOM. - Per-frame gameplay state — refs and module objects read inside `useFrame`. This covers scalars (positions, velocities, timers, mid-run combo counters) AND entity collections with their mutable fields: the live arrays of enemies/towers/projectiles and each entity's hp/target/cooldown. Never React state — beyond re-render stutter, `useFrame` callbacks capture the array from the render that created them, so a tower reading a useState `enemies` list targets a STALE snapshot: damage lands on ghosts and kills never register. Writing game logic as `setState(prev => ...)` updaters (or nesting one setState inside another) is the same failure. -- When state must be BOTH (read every frame in gameplay AND shown reactively in UI), install `zustand` with `install_npm_package`: components subscribe with the hook selector, `useFrame` reads via `getState()` without re-rendering. For simple games, EventBus + a module object is enough — don't add zustand for a single score counter. +- When state must be BOTH (read every frame in gameplay AND shown reactively in UI), install `zustand` with `npm install`: components subscribe with the hook selector, `useFrame` reads via `getState()` without re-rendering. For simple games, EventBus + a module object is enough — don't add zustand for a single score counter. - Systems that iterate many entities each frame share ONE live registry, mutated in place: ```js // src/game/3d/world.js — module scope (or one useRef/context at the scene root) diff --git a/skills/base44-platform-games/references/threejs/vfx_and_juice.md b/skills/base44-platform-games/references/threejs/vfx_and_juice.md index 41c0b84..049411e 100644 --- a/skills/base44-platform-games/references/threejs/vfx_and_juice.md +++ b/skills/base44-platform-games/references/threejs/vfx_and_juice.md @@ -31,6 +31,6 @@ drei `<PositionalAudio>` attaches 3D sound to an object (`loop` for ambience; ca </win_moments> <postprocessing> -For bloom (glowing projectiles/neon), vignette, or chromatic aberration, install `@react-three/postprocessing` with `install_npm_package`: one `<EffectComposer>` as the last child inside the Canvas, with effect components inside (all effects in the composer merge into a single pass). For selective glow, set `luminanceThreshold` to 1 on `<Bloom>` (with `mipmapBlur`) so only overbright objects bloom, and make materials overbright via `emissive` + `emissiveIntensity` above 1 (or `toneMapped={false}` on plain colors). Skip postprocessing on visibly low-end targets (pair with PerformanceMonitor). +For bloom (glowing projectiles/neon), vignette, or chromatic aberration, install `@react-three/postprocessing` with `npm install`: one `<EffectComposer>` as the last child inside the Canvas, with effect components inside (all effects in the composer merge into a single pass). For selective glow, set `luminanceThreshold` to 1 on `<Bloom>` (with `mipmapBlur`) so only overbright objects bloom, and make materials overbright via `emissive` + `emissiveIntensity` above 1 (or `toneMapped={false}` on plain colors). Skip postprocessing on visibly low-end targets (pair with PerformanceMonitor). </postprocessing> </vfx_and_juice> diff --git a/skills/base44-platform-manifest.json b/skills/base44-platform-manifest.json index 3c8a428..06aa80f 100644 --- a/skills/base44-platform-manifest.json +++ b/skills/base44-platform-manifest.json @@ -1,14 +1,14 @@ { "files": { "skills/base44-platform-actors/SKILL.md": "c3844ddcf866e7c4f1adb922a7fec64c0fd44427d9356f4ea104b08f031a9f13", - "skills/base44-platform-actors/references/mobile.md": "ed1071e7cb1af1169db8fcca1758cf03285ef7e1d345c8d601486a2782b0a5b6", - "skills/base44-platform-actors/references/web.md": "9114381a0228fbfab490679ecbf5b0d5129993a8e901d51175cfe31124a68c85", + "skills/base44-platform-actors/references/mobile.md": "31cc9bf4dcb7aacc9e8f98df8020c86130d0c14f719518945d6613d29798dbfe", + "skills/base44-platform-actors/references/web.md": "de7d5e7aea53b8a9e1f4eb89a75298d49a72da1b923215e87779926c6b7cf655", "skills/base44-platform-agents/SKILL.md": "c791b870ff47eeafedcf2604e42b57b823043856d23d1cf657fb2198333cdb5f", - "skills/base44-platform-agents/references/web-spa.md": "a99c7b5b6c93b0033d253574b5a94e7b8871953cd26d6c24c5c2bde68781d990", - "skills/base44-platform-agents/references/web-tanstack.md": "6c8b9fc5af17b71b516902febca8deca1a66ca334ef6678da0170998ebee426c", + "skills/base44-platform-agents/references/web-spa.md": "d429d967bd32996e597ce1386a8d255972f79080545536b8e9a5bca2322048c0", + "skills/base44-platform-agents/references/web-tanstack.md": "e48e0c5e2373ddcad47c02722b4b5dc5971640536b30e99896ef5113f97f4d74", "skills/base44-platform-backend-functions/SKILL.md": "0b1034eb6965a723fd10b5b70fa4616e2297938087a019fc8fce30d10e27f794", - "skills/base44-platform-backend-functions/references/mobile.md": "0163f74762ed3ea0b19f81d7f635e6dc74c0902c3b3456f1187883ea18f91512", - "skills/base44-platform-backend-functions/references/web-spa.md": "3806589571b0a3d4c6a665f0672ab39ff4ea06c6c10b996a30c2991d593df2f6", + "skills/base44-platform-backend-functions/references/mobile.md": "5ab98bac2e9d8056aba236d51f3965eb3f402b4306c0225b7f96fba28a8507a0", + "skills/base44-platform-backend-functions/references/web-spa.md": "dda2db002f201d15f5374820231524dc503ed3a4b9560e7833236c4c857eda9d", "skills/base44-platform-backend-functions/references/web-tanstack.md": "ad209a49e50b5668b479b98f13ec903da5554d9b540e3b10c12d4851898309c6", "skills/base44-platform-connectors/SKILL.md": "278579146b5e422f11cb15a9b33e5955cd9cbb62f0791c4cd40ca49535492a51", "skills/base44-platform-connectors/references/mobile.md": "cc30ab45866ebb4952fd8ec45954a756d7b82985dc12704cd8ff3118d330afb3", @@ -17,13 +17,13 @@ "skills/base44-platform-entities/SKILL.md": "5e40879dbb791b271953e8decc5b80ed63e950668b9522b65b45eb9c3647ad62", "skills/base44-platform-entities/references/mobile-and-games.md": "180accec4af5547da524e09fed6ed36414ea3d64f24cf622e40654a51565bb8a", "skills/base44-platform-entities/references/rls.md": "a87a71751bb63c28a0629c2ca65019c726c3b549149a029f74f5a84cb86b8dcf", - "skills/base44-platform-entities/references/web.md": "237aab59548b57ac6d20ef8ca6efeb0c50e42c72f16158fd6711e104c090a031", + "skills/base44-platform-entities/references/web.md": "e30672b094b3b5ed2fc926b06ee3646a8f1777491cae41cfb9a0be65f8bb1a9b", "skills/base44-platform-frontend/SKILL.md": "cfb5241785f6f0b6cba3cf1c6206849e19e20a9f46d1851a37afe840cbd90e59", "skills/base44-platform-frontend/references/mobile.md": "ecf9499821d8cf345e8b32143f2d0f3e72a4daeda06588a251d26f546bcac60a", "skills/base44-platform-frontend/references/web-spa.md": "410aee9583f1cf24dead07d4429d27ca7063e38053d50aa1cf7bc2a88305c56b", "skills/base44-platform-frontend/references/web-tanstack.md": "5c4227b1fc2fa80f2da40dff60e68f67f8c0054cdbf7758bc43bb6cea49d6614", "skills/base44-platform-games/SKILL.md": "4338b805e5e066045e015d26d5c4bb9aacfb077aa828095f723608b11662abbf", - "skills/base44-platform-games/references/game-2d.md": "242033f3bfff4e45630d68c232d9a576d6a74d3a794001413937398af734a13b", + "skills/base44-platform-games/references/game-2d.md": "2026065231813e8420e26bbdcd96928e443141fd49cbf006ca064a4f8e7eb006", "skills/base44-platform-games/references/game-3d.md": "782a89cb6cab7355b5e2016cb401e0f0966082853b5a96fb5ff876807d5eee39", "skills/base44-platform-games/references/phaser/actions_and_utilities.md": "66e7414bbb95280f91bd36dd933a578bbf6dfe8e6de284a17de774bd3da1c759", "skills/base44-platform-games/references/phaser/animations.md": "4ad18783e90621a36e4dc059cbed212a5e8159a1b09b41db8b157a34e481966f", @@ -54,9 +54,9 @@ "skills/base44-platform-games/references/phaser/tweens.md": "c5a7062d488039081ed7b24013fa13e78dea24125c7b972cfcb296436db6d0c5", "skills/base44-platform-games/references/threejs/assets_models_textures.md": "e011402834044d6e9205df7656726baceaa6aa98d9ade20f586d5ddce9236f29", "skills/base44-platform-games/references/threejs/cameras.md": "31b598dd5c6b8fa6e40298a26e72fe5260fd1d9020dd768314a12369f8955bea", - "skills/base44-platform-games/references/threejs/character_controller.md": "626b77a397f0362f08b183b2f8bd627ed513a43a7cb8f74b3b993def3f652c0f", + "skills/base44-platform-games/references/threejs/character_controller.md": "12f150f38fc51865846b455f43d18b88aeb525e61f26e80f74536797ff7a3bd8", "skills/base44-platform-games/references/threejs/enemies_and_movement.md": "e43c30fb7bed6d33749d483a1aa563ec3b205dcf76ad22d083c9f4c6eaa81dac", - "skills/base44-platform-games/references/threejs/game_state_and_flow.md": "bdfc7a33b0dc979a9d1b39c6935e04ee81485773b1d201883036ac5e571a17cf", + "skills/base44-platform-games/references/threejs/game_state_and_flow.md": "9f4224f5453e30df5a206010fb8db8c3a02eadbeb6b4de569901be17bc566ef5", "skills/base44-platform-games/references/threejs/input_and_controls.md": "56bd480ea000ad359e644d3b1811bbefe16a0843a4ae4d947ee06fc1c9c6e494", "skills/base44-platform-games/references/threejs/performance.md": "ed57ede1db9439ee663e71c6d4b2e579c8956c960fe69af79c5e8e0f57a11e4d", "skills/base44-platform-games/references/threejs/physics_dynamics.md": "75f0017a31867a4fb6a975393c099143ff57e1e031ad277af04c88662f73241c", @@ -64,7 +64,7 @@ "skills/base44-platform-games/references/threejs/scene_and_rendering.md": "667ea68cda6b354ffb6d54a7ce5050a661e8a997d3255f1c664440b00bd0cc30", "skills/base44-platform-games/references/threejs/text_hud_ui.md": "c9bef2170523bf1c679f1608730004fe9c6e9b02254cc1be2102113983d09658", "skills/base44-platform-games/references/threejs/triggers_and_collisions.md": "f981ba3c28e7f65f11e9f01e082ba24490ddb7e0c5aa9ee961d008a421f47758", - "skills/base44-platform-games/references/threejs/vfx_and_juice.md": "bada426d6e940194b5d476ac2958940bc7a012e7743ea7a6f7bdd702ca35006b", + "skills/base44-platform-games/references/threejs/vfx_and_juice.md": "84e3e98d8b72fe01b73b9c1da4b3c77a38f85a0500a2ceb54f18b5fc4cf73b4c", "skills/base44-platform-integrations-sdk/SKILL.md": "eea0a0061264ed71408db5b38787556d892472a13f2995d8c70464942bc143c2", "skills/base44-platform-integrations-sdk/references/mobile.md": "824d6a81dcfb5eff3dd51c48debf3324b5db805db21f44884b75e834abe1a599", "skills/base44-platform-integrations-sdk/references/web-spa.md": "09d9fe218764e0f116dc725fb76cd54559dc5fdd34aa3cc0d14663757343d423", @@ -75,9 +75,9 @@ "skills/base44-platform-platform/references/web-spa.md": "52917f4b90d778d21e73b70ab7862e9250044831b51db4dd8723cf68a6706941", "skills/base44-platform-platform/references/web-tanstack.md": "625c2c6f75647c843292b9a864db8b737b674848cae1d9e44e9630185c8429d9", "skills/base44-platform-workflows/SKILL.md": "c48b355ef3298ac81c662c4bb5bffb40486ede2ea42405d8ff00d3a01d46fae9", - "skills/base44-platform-workflows/references/automations-legacy.md": "8646424e87b879805c917726bbdb8c025ae66a1353e1d660bb9af6df5e561202", - "skills/base44-platform-workflows/references/mobile-automations.md": "80f00126db3869fa60919c4fac18cb6d8bcaa93019338a75e7a41e78a4f0249a", - "skills/base44-platform-workflows/references/web-spa.md": "f3f7f30a1ac50c60ef47c94526c662587aa6516bc639560de55e3cde71af62cc", - "skills/base44-platform-workflows/references/web-tanstack.md": "8102264e570c95f712c20f73f8d724b044ff33a5a0953a918c9a944ba5b36445" + "skills/base44-platform-workflows/references/automations-legacy.md": "0518e4aaab82d2e2ff842fce5eb7daeb140900eb48a952a3528e7cc84e2db502", + "skills/base44-platform-workflows/references/mobile-automations.md": "c122edd2de022cd63bffa54272ffd32d0d893544cdf82db216ea8b39f15a0a15", + "skills/base44-platform-workflows/references/web-spa.md": "1fa9695d74b1f5909eecea49dfaa75580e6d458528cbada64a9bca7830dba139", + "skills/base44-platform-workflows/references/web-tanstack.md": "5c72259395539dfc93628677d1ed0b00a5a886d85524d4e128318a6066ee155a" } } diff --git a/skills/base44-platform-workflows/references/automations-legacy.md b/skills/base44-platform-workflows/references/automations-legacy.md index d9fa4f6..0e4041b 100644 --- a/skills/base44-platform-workflows/references/automations-legacy.md +++ b/skills/base44-platform-workflows/references/automations-legacy.md @@ -27,7 +27,7 @@ Ends: ends_type="never" (default) | "on" + ends_on_date="2024-12-31T23:59:59" | <entity_automations> automation_type="entity" — fires on entity record changes. Required: entity_name, event_types (array of "create", "update", "delete"). -Function payload: event { type, entity_name, entity_id }; data: current entity data; old_data: previous data ("update" only); changed_fields: top-level fields that changed (e.g. ["status", "amount"]); payload_too_large: true if entity data exceeded 200KB — data/old_data then null, fetch with `await base44.entities.get("entity_name", entity_id)`. +Function payload: event { type, entity_name, entity_id }; data: current entity data; old_data: previous data ("update" only); changed_fields: top-level fields that changed (e.g. ["status", "amount"]); payload_too_large: true if entity data exceeded 200KB — data/old_data then null, fetch with `await base44.asServiceRole.entities[event.entity_name].get(event.entity_id)`. Optional trigger_conditions pre-filter entity events. Conditions evaluate against a wrapped object (entity automations only, not connector): {"data": {...current...}, "old_data": {...previous, update only...}, "changed_fields": [...]}. Field paths take the wrapping prefix: "data.<field>", "old_data.<field>", or "changed_fields" with operator "contains" to test whether a field changed. diff --git a/skills/base44-platform-workflows/references/mobile-automations.md b/skills/base44-platform-workflows/references/mobile-automations.md index d42b064..472d06e 100644 --- a/skills/base44-platform-workflows/references/mobile-automations.md +++ b/skills/base44-platform-workflows/references/mobile-automations.md @@ -58,7 +58,7 @@ The function receives a payload with: - data: current entity data (null if payload_too_large is true) - old_data: previous data (only for "update" events, null if payload_too_large is true) - changed_fields: array of top-level field names that changed between old_data and data (e.g. ["status", "amount"]) -- payload_too_large: true if entity data exceeded 200KB and was omitted. Use Base44 SDK to fetch: `await base44.entities.get("entity_name", entity_id)` +- payload_too_large: true if entity data exceeded 200KB and was omitted. Use Base44 SDK to fetch: `await base44.asServiceRole.entities[event.entity_name].get(event.entity_id)` Optional: trigger_conditions — pre-filter which entity events actually trigger execution. Use when the user only wants to act on specific changes (e.g., only when status becomes "completed", or only when a specific field changed). diff --git a/skills/base44-platform-workflows/references/web-spa.md b/skills/base44-platform-workflows/references/web-spa.md index ff26201..2ad1184 100644 --- a/skills/base44-platform-workflows/references/web-spa.md +++ b/skills/base44-platform-workflows/references/web-spa.md @@ -23,14 +23,11 @@ What workflows can be built from; the authoring guide that follows documents eac - app_publish — Fires when the app is published from the builder. - app_payment — Fires when a payment succeeds or is refunded, or a subscription is canceled, in the app's payment provider. - webhook — Fires on an inbound POST to a reusable workspace webhook URL, from any source. Use it for a third-party event no connector trigger covers - don't connect that provider or poll it to get the event. -- production_issue — Fires when a production issue the owner set an alert on happens again or crosses a threshold. ### Activities (what a `call` step can do) - app_security_scan — Security scan (same as the Security tab): `start` kicks it off, `results` reads the latest findings. - compute_seconds_until — Computes the seconds from now until a target datetime — feed the result into a dynamic wait. - invoke_backend_function — Runs a deployed backend function — your code for API calls, data transforms, and entity CRUD. -- publish_social_post — Publishes a scheduled post to the app's connected LinkedIn, Instagram or Facebook account. -- simulate_social_post — Simulate publishing a preview-only social post without contacting a social platform. ### Task Types (how steps are structured) - call — invoke a registered activity with `with` args, then transition via `then`. @@ -50,6 +47,8 @@ Each write creates a new version; running workflows keep their starting version. # Workflow Authoring Guide +> **Filtered guide** — documents ONLY triggers: scheduled, entity, connector, in_app_agent, app_user_auth, app_publish, app_payment, webhook; activities: app_security_scan, compute_seconds_until, invoke_backend_function. Any other trigger or activity is NOT covered here: do not author with it from memory — call this tool again with the names you need. + ## File Convention Workflows are `.jsonc` files under `base44/workflows/`. @@ -89,7 +88,7 @@ Change lifecycle status (activate / deactivate / archive / unarchive) in the Wor } ``` -**Task naming**: must be snake_case (letters, digits, underscores). Task results are accessed in jq as `${ .task_name.field }` — hyphens break jq field access. Never start a task name with `__` (double underscore) — that namespace is reserved for system steps and the save will be rejected. +**Task naming**: must be snake_case (letters, digits, underscores). Task results are accessed in jq as `${ .task_name.field }` — hyphens break jq field access. Never start a task name with `_` — that namespace is reserved for the system and the save will be rejected. **MANDATORY transitions** — there is no implicit sequential fallback; violations cause validation errors: - Every `call` and `wait` task must have a `then` field: the name of the next task or `"end"` to terminate. @@ -152,17 +151,6 @@ Call a deployed Deno backend function. - `args`: Dict of arguments passed as the function request body (supports jq expressions) - Returns: Parsed JSON response from the function -### publish_social_post -Publish a scheduled social post to the app's connected LinkedIn, Instagram or Facebook account. Takes the post created by create_scheduled_social_post. -- When to use: Posting to the app owner's connected social account on a schedule. Two steps, in order: call create_scheduled_social_post to store the content and get a scheduled_post_id, then author a workflow that passes that id here. The post is the source of truth for what gets published; the workflow's trigger decides when. Requires an active connector for the platform — create_scheduled_social_post checks and will tell you if the user needs to connect one first. Run now never publishes: it reports what would happen and leaves the post untouched. -- `scheduled_post_id`: ID returned by create_scheduled_social_post. Must belong to this app. -- Returns: {"scheduled_post_id": str, "status": str, "already_posted": bool, "external_post_id": str | null, "external_post_url": str | null, "dry_run": bool} - -### simulate_social_post -Simulate publishing a preview-only social post without contacting a social platform. -- `scheduled_post_id`: ID returned by create_scheduled_social_post. Must belong to this app. -- Returns: {"scheduled_post_id": str, "status": str, "already_posted": bool, "external_post_id": str | null, "external_post_url": str | null, "dry_run": bool} - ## Choosing Between Activities See each activity's "When to use" line above for what it's best at. @@ -261,7 +249,7 @@ Condition examples: - Only for a specific entity value: `"condition": "${ .trigger.data.priority == \"high\" }"` -Note: when the entity record is very large (over ~256KB), `data` and `old_data` arrive as null and `payload_too_large` is true (`changed_fields` stays populated). Trigger conditions still evaluate against the full data, but the backend function must re-fetch the record by id using the Base44 SDK: `await base44.entities.get("entity_name", entity_id)`. +Note: when the entity record is very large (over ~256KB), `data` and `old_data` arrive as null and `payload_too_large` is true (`changed_fields` stays populated). Trigger conditions still evaluate against the full data, but the backend function must re-fetch the record by id using the Base44 SDK: `await base44.asServiceRole.entities["entity_name"].get(entity_id)`. Use `read_file("base44/entities/<EntityName>.jsonc")` to check entity field names before writing conditions. @@ -396,7 +384,7 @@ Valid events: `payment_succeeded` (a successful payment: a completed checkout an Use `payment_refunded` and `subscription_canceled` to revoke what a purchase granted: match the event to the purchase the app saved by whichever of `subscription_id`, `checkout_id` and `payment_id` it carries, and make the revoke idempotent. -Recurring subscription renewal charges are wired to fire `payment_succeeded` too, but delivery is not enabled yet for existing Stripe accounts (it needs a Stripe app permission + per-account re-consent). Do not build a workflow whose only purpose is reacting to renewals, and tell the user renewals may not fire yet if they ask for one. The same holds for `subscription_canceled` on Stripe; on PayPal both fire. +Recurring subscription renewal charges are wired to fire `payment_succeeded` too, but delivery is not enabled yet for every Stripe account. Do not build a workflow whose only purpose is reacting to renewals, and tell the user renewals may not fire yet if they ask for one. The same holds for `subscription_canceled` on Stripe; on PayPal both fire. When renewals do arrive, the payload does not distinguish them from a first subscription payment — both are `payment_succeeded` with `mode: "subscription"`. So do not use this trigger for one-time-only actions on a subscription (welcome emails, provisioning, granting access) without your own guard, e.g. an entity record keyed by `provider_customer_id` that the workflow checks and writes. @@ -427,8 +415,8 @@ Condition examples: ### webhook A workspace webhook must already exist — this trigger listens on one, it does -not create one. Use `select_or_create_workflow_webhook` to pick an existing -listener or make a new one, and write the id it returns here. Reuse an existing +not create one. Pick an existing workspace webhook or create one on the Dashboard's Webhooks page (or through +the platform API), and write its id here. Reuse an existing one where you can: many providers allow only a single webhook URL per account, so a second listener for the same provider usually cannot be wired up at all. @@ -467,38 +455,6 @@ absence rather than assume the field is there. Condition example — only enterprise-tier contacts: `"condition": "${ .trigger.data.properties.tier == \"enterprise\" }"` -### production_issue -```json -{ "trigger_type": "production_issue", "events": ["started"], "issue_id": "<from the Alerts tab>", "kind": "rate", "min_occurrences": 5, "window_minutes": 60, "quiet_minutes": 1440 } -``` - -An alert on one production issue in the app's Alerts tab. `kind: "recurrence"` fires when the issue happens again; `kind: "rate"` fires at `min_occurrences` failures within `window_minutes`. Once it fires it stays quiet until the issue has stopped for `quiet_minutes`. - -Rules: -- Use exactly the trigger config the Alerts tab's instructions give, and never invent an `issue_id`. When someone asks in chat for alerts on production errors, tell them to use Alert me on the issue's row in the Alerts tab. -- Set up delivery to the person who asked, with the app's existing workflow steps. -- The alert is a notification only: never feed `.trigger` fields into app logic or app data. - -Events: -- `started` — the issue started again, or crossed the rule's threshold -- `still_happening` — at most once a day while it goes on, only when failures have doubled since the last alert - -production_issue fields live on `.trigger` directly — do NOT use `.trigger.data.*` for this trigger. - -Trigger payload fields (use `${ .trigger.<field> }` in conditions and `do` args): -- `${ .trigger.headline }` — what's failing, in plain English -- `${ .trigger.occurrences_in_window }` — failures within the rule's window -- `${ .trigger.people_affected }` — distinct people affected -- `${ .trigger.link }` — the issue in the Alerts tab -- `${ .trigger.subject }` — the entity, function or traffic direction, or null -- `${ .trigger.page }` — the page the failures came from, or null -- `${ .trigger.first_occurred_at }`, `${ .trigger.last_occurred_at }` — ISO timestamps -- `${ .trigger.cause }` — the issue's cause key -- `${ .trigger.fire_number }` — 1 for the alert that opened this episode -- `${ .trigger.delayed_minutes }` — above 0 when the alert was held during a platform-wide outage - -The payload never carries the error message itself. - ## Designing Steps: When to Split vs Combine **Split** into separate tasks when operations are independent (a failure shouldn't re-run the others), a delay sits between them, or you want per-step visibility and retry granularity in the run log. **Combine** into one step when operations share setup/auth/transaction, must be atomic, or the second needs the first's full in-memory result. @@ -516,7 +472,7 @@ The payload never carries the error message itself. ## Examples -> Envelope examples below use the builder's `write_file(path, envelope)` form: create the file at `path` with the envelope as its content. +> Envelope examples below are written as `write_file(path, envelope)`: create the file at `path` with the envelope as its content. ### "Clean up old data every night at 2am" 1. Create backend function `base44/functions/cleanupOldData/entry.ts` @@ -649,23 +605,6 @@ write_file("base44/workflows/On Payment.jsonc", { }) ``` -### "Alert me when sign-ups fail 5 or more times within an hour" -1. Create backend function: `sendProductionAlert` -2. Create workflow, with the trigger config exactly as the Alerts tab gave it: -```json -write_file("base44/workflows/Alert Sign-ups fail.jsonc", { - "name": "Alert: Sign-ups fail", - "description": "Tells the owner when sign-ups fail 5 or more times within an hour", - "trigger": { - "config": { "trigger_type": "production_issue", "events": ["started"], "issue_id": "6650f0c2a1b2c3d4e5f60718", "kind": "rate", "min_occurrences": 5, "window_minutes": 60, "quiet_minutes": 1440 } - }, - "definition": { - "document": { "dsl": "1.0.0", "name": "alert_signups_fail", "version": "1.0", "namespace": "base44" }, - "do": [{ "send_alert": { "call": "invoke_backend_function", "with": { "function_name": "sendProductionAlert", "args": { "headline": "${ .trigger.headline }", "count": "${ .trigger.occurrences_in_window }", "link": "${ .trigger.link }" } }, "then": "end" } }] - } -}) -``` - ### Refresh, then act on the findings — the start / wait / results pattern (any trigger) Point the `ready` branch at whatever step consumes `${ .read.scan }` (its `findings`, `total_findings`, `risk_level`). ```json @@ -682,12 +621,3 @@ Point the `ready` branch at whatever step consumes `${ .read.scan }` (its `findi { "read_again": { "call": "app_security_scan", "with": { "action": "results", "after": "${ .scan.requested_at }" }, "then": "end" } } ] ``` - -### "Post our launch announcement to LinkedIn on Tuesday morning" — the two-step pattern -1. Call `create_scheduled_social_post` with the platform, copy, and time. It verifies the LinkedIn connector and returns a `scheduled_post_id`. -2. Author a workflow on a schedule trigger set to that time, passing the returned id: -```json -"do": [ - { "publish": { "call": "publish_social_post", "with": { "scheduled_post_id": "sp_a1b2c3" }, "then": "end" } } -] -``` diff --git a/skills/base44-platform-workflows/references/web-tanstack.md b/skills/base44-platform-workflows/references/web-tanstack.md index 670e287..90f3b56 100644 --- a/skills/base44-platform-workflows/references/web-tanstack.md +++ b/skills/base44-platform-workflows/references/web-tanstack.md @@ -23,14 +23,11 @@ What workflows can be built from; the authoring guide that follows documents eac - app_publish — Fires when the app is published from the builder. - app_payment — Fires when a payment succeeds or is refunded, or a subscription is canceled, in the app's payment provider. - webhook — Fires on an inbound POST to a reusable workspace webhook URL, from any source. Use it for a third-party event no connector trigger covers - don't connect that provider or poll it to get the event. -- production_issue — Fires when a production issue the owner set an alert on happens again or crosses a threshold. ### Activities (what a `call` step can do) - app_security_scan — Security scan (same as the Security tab): `start` kicks it off, `results` reads the latest findings. - compute_seconds_until — Computes the seconds from now until a target datetime — feed the result into a dynamic wait. - invoke_server_route — Calls a route on the app's own server. -- publish_social_post — Publishes a scheduled post to the app's connected LinkedIn, Instagram or Facebook account. -- simulate_social_post — Simulate publishing a preview-only social post without contacting a social platform. ### Task Types (how steps are structured) - call — invoke a registered activity with `with` args, then transition via `then`. @@ -50,6 +47,8 @@ Each write creates a new version; running workflows keep their starting version. # Workflow Authoring Guide +> **Filtered guide** — documents ONLY triggers: scheduled, entity, connector, in_app_agent, app_user_auth, app_publish, app_payment, webhook; activities: app_security_scan, compute_seconds_until, invoke_server_route. Any other trigger or activity is NOT covered here: do not author with it from memory — call this tool again with the names you need. + ## File Convention Workflows are `.jsonc` files under `base44/workflows/`. @@ -89,7 +88,7 @@ Change lifecycle status (activate / deactivate / archive / unarchive) in the Wor } ``` -**Task naming**: must be snake_case (letters, digits, underscores). Task results are accessed in jq as `${ .task_name.field }` — hyphens break jq field access. Never start a task name with `__` (double underscore) — that namespace is reserved for system steps and the save will be rejected. +**Task naming**: must be snake_case (letters, digits, underscores). Task results are accessed in jq as `${ .task_name.field }` — hyphens break jq field access. Never start a task name with `_` — that namespace is reserved for the system and the save will be rejected. **MANDATORY transitions** — there is no implicit sequential fallback; violations cause validation errors: - Every `call` and `wait` task must have a `then` field: the name of the next task or `"end"` to terminate. @@ -154,17 +153,6 @@ Call a route served by the app's own full-stack server. - `query`: Dict of query-string parameters (supports jq expressions) - Returns: Parsed JSON response from the route -### publish_social_post -Publish a scheduled social post to the app's connected LinkedIn, Instagram or Facebook account. Takes the post created by create_scheduled_social_post. -- When to use: Posting to the app owner's connected social account on a schedule. Two steps, in order: call create_scheduled_social_post to store the content and get a scheduled_post_id, then author a workflow that passes that id here. The post is the source of truth for what gets published; the workflow's trigger decides when. Requires an active connector for the platform — create_scheduled_social_post checks and will tell you if the user needs to connect one first. Run now never publishes: it reports what would happen and leaves the post untouched. -- `scheduled_post_id`: ID returned by create_scheduled_social_post. Must belong to this app. -- Returns: {"scheduled_post_id": str, "status": str, "already_posted": bool, "external_post_id": str | null, "external_post_url": str | null, "dry_run": bool} - -### simulate_social_post -Simulate publishing a preview-only social post without contacting a social platform. -- `scheduled_post_id`: ID returned by create_scheduled_social_post. Must belong to this app. -- Returns: {"scheduled_post_id": str, "status": str, "already_posted": bool, "external_post_id": str | null, "external_post_url": str | null, "dry_run": bool} - ## Choosing Between Activities See each activity's "When to use" line above for what it's best at. @@ -263,7 +251,7 @@ Condition examples: - Only for a specific entity value: `"condition": "${ .trigger.data.priority == \"high\" }"` -Note: when the entity record is very large (over ~256KB), `data` and `old_data` arrive as null and `payload_too_large` is true (`changed_fields` stays populated). Trigger conditions still evaluate against the full data, but the server route must re-fetch the record by id using the Base44 SDK: `await base44.entities.get("entity_name", entity_id)`. +Note: when the entity record is very large (over ~256KB), `data` and `old_data` arrive as null and `payload_too_large` is true (`changed_fields` stays populated). Trigger conditions still evaluate against the full data, but the server route must re-fetch the record by id using the Base44 SDK: `await base44.asServiceRole.entities["entity_name"].get(entity_id)`. Use `read_file("base44/entities/<EntityName>.jsonc")` to check entity field names before writing conditions. @@ -398,7 +386,7 @@ Valid events: `payment_succeeded` (a successful payment: a completed checkout an Use `payment_refunded` and `subscription_canceled` to revoke what a purchase granted: match the event to the purchase the app saved by whichever of `subscription_id`, `checkout_id` and `payment_id` it carries, and make the revoke idempotent. -Recurring subscription renewal charges are wired to fire `payment_succeeded` too, but delivery is not enabled yet for existing Stripe accounts (it needs a Stripe app permission + per-account re-consent). Do not build a workflow whose only purpose is reacting to renewals, and tell the user renewals may not fire yet if they ask for one. The same holds for `subscription_canceled` on Stripe; on PayPal both fire. +Recurring subscription renewal charges are wired to fire `payment_succeeded` too, but delivery is not enabled yet for every Stripe account. Do not build a workflow whose only purpose is reacting to renewals, and tell the user renewals may not fire yet if they ask for one. The same holds for `subscription_canceled` on Stripe; on PayPal both fire. When renewals do arrive, the payload does not distinguish them from a first subscription payment — both are `payment_succeeded` with `mode: "subscription"`. So do not use this trigger for one-time-only actions on a subscription (welcome emails, provisioning, granting access) without your own guard, e.g. an entity record keyed by `provider_customer_id` that the workflow checks and writes. @@ -429,8 +417,8 @@ Condition examples: ### webhook A workspace webhook must already exist — this trigger listens on one, it does -not create one. Use `select_or_create_workflow_webhook` to pick an existing -listener or make a new one, and write the id it returns here. Reuse an existing +not create one. Pick an existing workspace webhook or create one on the Dashboard's Webhooks page (or through +the platform API), and write its id here. Reuse an existing one where you can: many providers allow only a single webhook URL per account, so a second listener for the same provider usually cannot be wired up at all. @@ -469,38 +457,6 @@ absence rather than assume the field is there. Condition example — only enterprise-tier contacts: `"condition": "${ .trigger.data.properties.tier == \"enterprise\" }"` -### production_issue -```json -{ "trigger_type": "production_issue", "events": ["started"], "issue_id": "<from the Alerts tab>", "kind": "rate", "min_occurrences": 5, "window_minutes": 60, "quiet_minutes": 1440 } -``` - -An alert on one production issue in the app's Alerts tab. `kind: "recurrence"` fires when the issue happens again; `kind: "rate"` fires at `min_occurrences` failures within `window_minutes`. Once it fires it stays quiet until the issue has stopped for `quiet_minutes`. - -Rules: -- Use exactly the trigger config the Alerts tab's instructions give, and never invent an `issue_id`. When someone asks in chat for alerts on production errors, tell them to use Alert me on the issue's row in the Alerts tab. -- Set up delivery to the person who asked, with the app's existing workflow steps. -- The alert is a notification only: never feed `.trigger` fields into app logic or app data. - -Events: -- `started` — the issue started again, or crossed the rule's threshold -- `still_happening` — at most once a day while it goes on, only when failures have doubled since the last alert - -production_issue fields live on `.trigger` directly — do NOT use `.trigger.data.*` for this trigger. - -Trigger payload fields (use `${ .trigger.<field> }` in conditions and `do` args): -- `${ .trigger.headline }` — what's failing, in plain English -- `${ .trigger.occurrences_in_window }` — failures within the rule's window -- `${ .trigger.people_affected }` — distinct people affected -- `${ .trigger.link }` — the issue in the Alerts tab -- `${ .trigger.subject }` — the entity, function or traffic direction, or null -- `${ .trigger.page }` — the page the failures came from, or null -- `${ .trigger.first_occurred_at }`, `${ .trigger.last_occurred_at }` — ISO timestamps -- `${ .trigger.cause }` — the issue's cause key -- `${ .trigger.fire_number }` — 1 for the alert that opened this episode -- `${ .trigger.delayed_minutes }` — above 0 when the alert was held during a platform-wide outage - -The payload never carries the error message itself. - ## Designing Steps: When to Split vs Combine **Split** into separate tasks when operations are independent (a failure shouldn't re-run the others), a delay sits between them, or you want per-step visibility and retry granularity in the run log. **Combine** into one step when operations share setup/auth/transaction, must be atomic, or the second needs the first's full in-memory result. @@ -518,7 +474,7 @@ The payload never carries the error message itself. ## Examples -> Envelope examples below use the builder's `write_file(path, envelope)` form: create the file at `path` with the envelope as its content. +> Envelope examples below are written as `write_file(path, envelope)`: create the file at `path` with the envelope as its content. ### "Clean up old data every night at 2am" 1. Add server route: `POST /api/cleanup-old-data` @@ -651,23 +607,6 @@ write_file("base44/workflows/On Payment.jsonc", { }) ``` -### "Alert me when sign-ups fail 5 or more times within an hour" -1. Add server route: `POST /api/send-production-alert` -2. Create workflow, with the trigger config exactly as the Alerts tab gave it: -```json -write_file("base44/workflows/Alert Sign-ups fail.jsonc", { - "name": "Alert: Sign-ups fail", - "description": "Tells the owner when sign-ups fail 5 or more times within an hour", - "trigger": { - "config": { "trigger_type": "production_issue", "events": ["started"], "issue_id": "6650f0c2a1b2c3d4e5f60718", "kind": "rate", "min_occurrences": 5, "window_minutes": 60, "quiet_minutes": 1440 } - }, - "definition": { - "document": { "dsl": "1.0.0", "name": "alert_signups_fail", "version": "1.0", "namespace": "base44" }, - "do": [{ "send_alert": { "call": "invoke_server_route", "with": { "path": "/api/send-production-alert", "body": { "headline": "${ .trigger.headline }", "count": "${ .trigger.occurrences_in_window }", "link": "${ .trigger.link }" } }, "then": "end" } }] - } -}) -``` - ### Refresh, then act on the findings — the start / wait / results pattern (any trigger) Point the `ready` branch at whatever step consumes `${ .read.scan }` (its `findings`, `total_findings`, `risk_level`). ```json @@ -684,12 +623,3 @@ Point the `ready` branch at whatever step consumes `${ .read.scan }` (its `findi { "read_again": { "call": "app_security_scan", "with": { "action": "results", "after": "${ .scan.requested_at }" }, "then": "end" } } ] ``` - -### "Post our launch announcement to LinkedIn on Tuesday morning" — the two-step pattern -1. Call `create_scheduled_social_post` with the platform, copy, and time. It verifies the LinkedIn connector and returns a `scheduled_post_id`. -2. Author a workflow on a schedule trigger set to that time, passing the returned id: -```json -"do": [ - { "publish": { "call": "publish_social_post", "with": { "scheduled_post_id": "sp_a1b2c3" }, "then": "end" } } -] -``` From 7ff0e29d176c1d4671693e0561f788cd95aa54e6 Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Thu, 22 Oct 2026 12:52:46 +0000 Subject: [PATCH 08/15] sync: recompile after review (source 99a6de6078e) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflows guide now lists the connector webhook events (compiled with the connector seed installed), the external connector pointer says what list_connectors returns, secrets and connector instructions name the Dashboard → Secrets and Workspace settings → Connectors pages, and CONTRIBUTING describes the hold-until-approved path for review findings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLKGecSgMqR14kYQTrugoR --- CONTRIBUTING.md | 10 +++++----- SOURCE_COMMIT | 2 +- .../references/mobile.md | 6 +++--- .../references/web-spa.md | 4 ++-- .../references/web-tanstack.md | 2 +- .../references/mobile.md | 2 +- .../references/web-spa.md | 2 +- .../references/web-tanstack.md | 2 +- skills/base44-platform-manifest.json | 16 ++++++++-------- .../references/web-spa.md | 13 +++++++++++-- .../references/web-tanstack.md | 13 +++++++++++-- 11 files changed, 45 insertions(+), 27 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1374d96..b5bfb38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -129,17 +129,17 @@ the in-editor AI learns it, one reference per app shape. 1. **At Base44**: the compiler renders every prompt partial in its capability matrix for the external surface, drops passages marked internal, and validates the result (deterministic output, no builder-tool vocabulary, no internal names, manifest, links). An IP review then reads the tree - that would be published; only a tree it clears, or one a Base44 engineer has settled, is synced. - The sync opens one PR here on the branch `auto/base44-platform-skills`, labeled - `auto-skill-update`, with the review's report in its body. No change, no PR. + that would be published; a tree it flags stays inside Base44 until an engineer has read the + findings and approved it. The sync opens one PR here on the branch `auto/base44-platform-skills`, + labeled `auto-skill-update`, with the review's report in its body. No change, no PR. 2. **In this repo**: `Validate base44-platform skills` (`scripts/validate-base44-platform.mjs`) checks that the PR carries exactly what the compiler emitted: the generated-file header on every file, sha256 hashes matching `skills/base44-platform-manifest.json`, frontmatter whose `name` equals the directory name, resolving links, and a 40-hex `SOURCE_COMMIT` pin. 3. `Auto-merge compiled skill updates` enables squash auto-merge on a labeled PR opened by `base44-github-actions[bot]` whose diff is confined to `skills/base44-platform-*/**` and - `SOURCE_COMMIT`. A PR the review opened as a draft waits for a human; so does everything else. - The Slack notifier posts the PR like any other skill-sync PR. + `SOURCE_COMMIT`. A PR published on an engineer's approval after review findings waits for a + human merge; so does everything else. The Slack notifier posts the PR like any other skill-sync PR. ### How to change a compiled skill diff --git a/SOURCE_COMMIT b/SOURCE_COMMIT index 059c257..090cd1a 100644 --- a/SOURCE_COMMIT +++ b/SOURCE_COMMIT @@ -1 +1 @@ -9fc7fc6ccc00148c3a2cf18206a3653108ac7fb0 +99a6de6078ee0e55b90bdd18c99a71d9058faab4 diff --git a/skills/base44-platform-backend-functions/references/mobile.md b/skills/base44-platform-backend-functions/references/mobile.md index adbe161..7223d52 100644 --- a/skills/base44-platform-backend-functions/references/mobile.md +++ b/skills/base44-platform-backend-functions/references/mobile.md @@ -42,13 +42,13 @@ For external webhooks, browser GET links and OAuth callbacks, use the server-set <guidelines> <main_guidelines> -1. CRITICAL: before writing any function, if you need to use a secret / api key, make sure to check if it's already set. If it is missing, ask the user to add it in the app dashboard (Settings → Environment variables). +1. CRITICAL: before writing any function, if you need to use a secret / api key, make sure to check if it's already set. If it is missing, ask the user to add it on the app's Dashboard → Secrets page. 2. ADMIN-ONLY FUNCTIONS: If a function should only be invoked by admin (e.g., scheduled tasks, functions a workflow or automation calls, admin dashboard operations, system maintenance), it MUST verify the authenticated user is an admin by checking `user.role === 'admin'` and return 403 Forbidden if not. 3. -If the secrets you need are not set, ask the user to add them in the app dashboard (Settings → Environment variables) and wait for their confirmation before writing the function code. +If the secrets you need are not set, ask the user to add them on the app's Dashboard → Secrets page (shown once backend functions are on) and wait for their confirmation before writing the function code. -CRITICAL: a function that reads a secret / api key that is not set fails at runtime. +A function that reads an unset secret or API key fails at runtime. For example, if the user asks for a WeatherAPI.com integration, you should request the WEATHER_API_KEY secret. diff --git a/skills/base44-platform-backend-functions/references/web-spa.md b/skills/base44-platform-backend-functions/references/web-spa.md index c1a7e0c..9851aa4 100644 --- a/skills/base44-platform-backend-functions/references/web-spa.md +++ b/skills/base44-platform-backend-functions/references/web-spa.md @@ -18,7 +18,7 @@ For external-service integrations, in order: </integration_priority> <secrets> -Before writing a function that needs a secret/API key, check it's set — using an unset secret fails. If missing, ask the user to add it on the app Dashboard's "Secrets" page (its own sidebar item, shown once backend functions are on) and wait for their confirmation before writing code that reads it. +Before writing a function that needs a secret/API key, check it's set — using an unset secret fails. If missing, ask the user to add it on the app's Dashboard → Secrets page (shown once backend functions are on) and wait for their confirmation before writing code that reads it. Secrets: `import { secrets } from "base44:runtime"` → `secrets.get("NAME")`; pre-populated: BASE44_APP_ID. Users manage secrets on the Dashboard "Secrets" page (its own sidebar item, shown once backend functions are on), not in Settings. Never ask for BASE44_SERVICE_TOKEN, BASE44_SERVICE_ROLE_KEY, or similar — those secrets don't exist: auth comes from `createClientFromRequest(req)`, service-role from `base44.asServiceRole`. </secrets> @@ -121,7 +121,7 @@ A function deploys automatically once its file is written. Test it by invoking i Integrate OpenAI's gpt-4o-mini via a backend function: tell the user a joke based on their name. </user_ask> <assistant> -I need the OPENAI_API_KEY secret first — please add it in the app dashboard (Settings → Environment variables). +I need the OPENAI_API_KEY secret first — please add it on the app's Dashboard → Secrets page. </assistant> <user_ask> I set the OPENAI_API_KEY secret. Please write the function. diff --git a/skills/base44-platform-backend-functions/references/web-tanstack.md b/skills/base44-platform-backend-functions/references/web-tanstack.md index 369009f..e572c0f 100644 --- a/skills/base44-platform-backend-functions/references/web-tanstack.md +++ b/skills/base44-platform-backend-functions/references/web-tanstack.md @@ -24,7 +24,7 @@ Server functions and server routes are public endpoints: anyone can call them, w </caller_auth> <secrets> -Before writing server code that needs an API key or secret, check it is set — an unset secret fails. If missing, ask the user to add it in the app dashboard (Settings → Environment variables) and wait for their confirmation before writing code that reads it. Read it with `import { secrets } from "base44:runtime"` → `secrets.get("NAME")` (`undefined` if unset) only in server functions, server routes and `*.server.js` files; never `process.env` or a `VITE_*` var (build-time, client-visible). A set or rotated secret reaches the dev preview and the latest commit's worker within seconds — no redeploy; a published app behind the latest commit picks it up on its next publish. +Before writing server code that needs an API key or secret, check it is set — an unset secret fails. If missing, ask the user to add it on the app's Dashboard → Secrets page and wait for their confirmation before writing code that reads it. Read it with `import { secrets } from "base44:runtime"` → `secrets.get("NAME")` (`undefined` if unset) only in server functions, server routes and `*.server.js` files; never `process.env` or a `VITE_*` var (build-time, client-visible). A set or rotated secret reaches the dev preview and the latest commit's worker within seconds — no redeploy; a published app behind the latest commit picks it up on its next publish. </secrets> <runtime> diff --git a/skills/base44-platform-connectors/references/mobile.md b/skills/base44-platform-connectors/references/mobile.md index d05f0db..1bf6d42 100644 --- a/skills/base44-platform-connectors/references/mobile.md +++ b/skills/base44-platform-connectors/references/mobile.md @@ -16,7 +16,7 @@ App connectors come in the following types: Backend functions MUST be enabled for each connector type. <connector_decision_flow> -Connecting a connector happens outside the code: read the catalog and each connector's details (description, scopes, auth status, connection mode) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available --app-id <APP_ID>`; connect or re-scope a shared connector with `initiate_connector_connection` / `base44 connectors initiate` passing the complete scope set (it replaces, never merges), and let the user finish OAuth in their browser. App-user and BYO-shared connectors are registered by a workspace admin in Settings → OAuth Connectors. +Connecting a connector happens outside the code: read the catalog and each connector's details (description, scopes, auth status, connection mode) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available --app-id <APP_ID>`; connect or re-scope a shared connector with `initiate_connector_connection` / `base44 connectors initiate` passing the complete scope set (it replaces, never merges), and let the user finish OAuth in their browser. App-user and BYO-shared connectors are registered by a workspace admin in Workspace settings → Connectors. </connector_decision_flow> <supported_connectors> diff --git a/skills/base44-platform-connectors/references/web-spa.md b/skills/base44-platform-connectors/references/web-spa.md index 3191b87..b320f49 100644 --- a/skills/base44-platform-connectors/references/web-spa.md +++ b/skills/base44-platform-connectors/references/web-spa.md @@ -13,7 +13,7 @@ App connectors integrate the app with external services via OAuth. </connector_types> <connector_decision_flow> -Connecting a connector happens outside the code: read the catalog and each connector's details (description, scopes, auth status, connection mode) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available --app-id <APP_ID>`; connect or re-scope a shared connector with `initiate_connector_connection` / `base44 connectors initiate` passing the complete scope set (it replaces, never merges), and let the user finish OAuth in their browser. App-user and BYO-shared connectors are registered by a workspace admin in Settings → OAuth Connectors. +Connecting a connector happens outside the code: read the catalog and each connector's details (description, scopes, auth status, connection mode) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available --app-id <APP_ID>`; connect or re-scope a shared connector with `initiate_connector_connection` / `base44 connectors initiate` passing the complete scope set (it replaces, never merges), and let the user finish OAuth in their browser. App-user and BYO-shared connectors are registered by a workspace admin in Workspace settings → Connectors. </connector_decision_flow> <supported_connectors> diff --git a/skills/base44-platform-connectors/references/web-tanstack.md b/skills/base44-platform-connectors/references/web-tanstack.md index 43d397e..e972d00 100644 --- a/skills/base44-platform-connectors/references/web-tanstack.md +++ b/skills/base44-platform-connectors/references/web-tanstack.md @@ -13,7 +13,7 @@ App connectors integrate the app with external services via OAuth. </connector_types> <connector_decision_flow> -Connecting a connector happens outside the code: read the catalog and each connector's details (description, scopes, auth status, connection mode) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available --app-id <APP_ID>`; connect or re-scope a shared connector with `initiate_connector_connection` / `base44 connectors initiate` passing the complete scope set (it replaces, never merges), and let the user finish OAuth in their browser. App-user and BYO-shared connectors are registered by a workspace admin in Settings → OAuth Connectors. +Connecting a connector happens outside the code: read the catalog and each connector's details (description, scopes, auth status, connection mode) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available --app-id <APP_ID>`; connect or re-scope a shared connector with `initiate_connector_connection` / `base44 connectors initiate` passing the complete scope set (it replaces, never merges), and let the user finish OAuth in their browser. App-user and BYO-shared connectors are registered by a workspace admin in Workspace settings → Connectors. </connector_decision_flow> <supported_connectors> diff --git a/skills/base44-platform-manifest.json b/skills/base44-platform-manifest.json index 06aa80f..5806a3b 100644 --- a/skills/base44-platform-manifest.json +++ b/skills/base44-platform-manifest.json @@ -7,13 +7,13 @@ "skills/base44-platform-agents/references/web-spa.md": "d429d967bd32996e597ce1386a8d255972f79080545536b8e9a5bca2322048c0", "skills/base44-platform-agents/references/web-tanstack.md": "e48e0c5e2373ddcad47c02722b4b5dc5971640536b30e99896ef5113f97f4d74", "skills/base44-platform-backend-functions/SKILL.md": "0b1034eb6965a723fd10b5b70fa4616e2297938087a019fc8fce30d10e27f794", - "skills/base44-platform-backend-functions/references/mobile.md": "5ab98bac2e9d8056aba236d51f3965eb3f402b4306c0225b7f96fba28a8507a0", - "skills/base44-platform-backend-functions/references/web-spa.md": "dda2db002f201d15f5374820231524dc503ed3a4b9560e7833236c4c857eda9d", - "skills/base44-platform-backend-functions/references/web-tanstack.md": "ad209a49e50b5668b479b98f13ec903da5554d9b540e3b10c12d4851898309c6", + "skills/base44-platform-backend-functions/references/mobile.md": "034fe28f812897363b91242078a1092943e319fdee0fc28ea8d2fd779deb8498", + "skills/base44-platform-backend-functions/references/web-spa.md": "43620240a07f0e0b776ebb7c38945fddecd806a23a963b7b3ec5fd2feeecbfaa", + "skills/base44-platform-backend-functions/references/web-tanstack.md": "960f4fede0d0c878e8e98b23c196ac9fcc99a7b9e2ef8f46cf005d607e73f0cd", "skills/base44-platform-connectors/SKILL.md": "278579146b5e422f11cb15a9b33e5955cd9cbb62f0791c4cd40ca49535492a51", - "skills/base44-platform-connectors/references/mobile.md": "cc30ab45866ebb4952fd8ec45954a756d7b82985dc12704cd8ff3118d330afb3", - "skills/base44-platform-connectors/references/web-spa.md": "7ef4ae7afc1c5b00d55fdfc1330fefad226411c0065c0d82a263e9a198aae482", - "skills/base44-platform-connectors/references/web-tanstack.md": "64ddd48bc69e877038d677ea666fecab6c37d8103f1f1acea61a00ff6111e1f8", + "skills/base44-platform-connectors/references/mobile.md": "7cb1dbad4829a203a0dc44bde8646410e3484c9fb45c64de0f7201d870e5aefd", + "skills/base44-platform-connectors/references/web-spa.md": "aa905eab8a95f97c85e4755123eae2b38c5e4e62a09c516d481519b51d04734c", + "skills/base44-platform-connectors/references/web-tanstack.md": "cec2fb0c272335ccb605f774bd3f8b621b37d3f8985c1e9ff4c56857a3d6b209", "skills/base44-platform-entities/SKILL.md": "5e40879dbb791b271953e8decc5b80ed63e950668b9522b65b45eb9c3647ad62", "skills/base44-platform-entities/references/mobile-and-games.md": "180accec4af5547da524e09fed6ed36414ea3d64f24cf622e40654a51565bb8a", "skills/base44-platform-entities/references/rls.md": "a87a71751bb63c28a0629c2ca65019c726c3b549149a029f74f5a84cb86b8dcf", @@ -77,7 +77,7 @@ "skills/base44-platform-workflows/SKILL.md": "c48b355ef3298ac81c662c4bb5bffb40486ede2ea42405d8ff00d3a01d46fae9", "skills/base44-platform-workflows/references/automations-legacy.md": "0518e4aaab82d2e2ff842fce5eb7daeb140900eb48a952a3528e7cc84e2db502", "skills/base44-platform-workflows/references/mobile-automations.md": "c122edd2de022cd63bffa54272ffd32d0d893544cdf82db216ea8b39f15a0a15", - "skills/base44-platform-workflows/references/web-spa.md": "1fa9695d74b1f5909eecea49dfaa75580e6d458528cbada64a9bca7830dba139", - "skills/base44-platform-workflows/references/web-tanstack.md": "5c72259395539dfc93628677d1ed0b00a5a886d85524d4e128318a6066ee155a" + "skills/base44-platform-workflows/references/web-spa.md": "20666c624bcd735ff88e0fba5dd09ed98c68c80ff9e0edf50061d92d9fb3fc6e", + "skills/base44-platform-workflows/references/web-tanstack.md": "679d1a60b5854952426f08f71001dd8e962ffd1a15e2c514a2f88c4045b848f7" } } diff --git a/skills/base44-platform-workflows/references/web-spa.md b/skills/base44-platform-workflows/references/web-spa.md index 2ad1184..8239bd4 100644 --- a/skills/base44-platform-workflows/references/web-spa.md +++ b/skills/base44-platform-workflows/references/web-spa.md @@ -265,7 +265,7 @@ Optional: `resource_id` — for Google Calendar (calendar ID, e.g. "primary" or Prerequisites: - The connector must be authorized (use the Base44 MCP connector tools `list_connectors` and `initiate_connector_connection`, or `base44 connectors`). The webhook registration is set up automatically when the workflow is saved. -- Each provider has a different payload structure and auth model. Always read the connector's usage guide (`list_connectors` with the integration type) for the provider-specific webhook handler guide BEFORE writing the backend function. +- Each provider has a different payload structure and auth model. Read the provider's own webhook documentation for its payload and auth model (`list_connectors` gives the integration type, scopes and connection state, not a handler guide) BEFORE writing the backend function. Trigger payload: - `${ .trigger.integration_type }` — e.g. "slack", "googlecalendar" @@ -284,7 +284,16 @@ High-volume connectors (e.g. Slack) require conditions — validation will rejec If activation fails with `MISSING_WEBHOOK_SCOPES`, re-connect that connector (`initiate_connector_connection`, or `base44 connectors initiate`) with the union of its granted and missing scopes (re-authorization replaces the grant, so omitting the granted ones drops them), then save the workflow again. Available connectors: - (no connectors with webhook support) + - gmail: mailbox + - googlecalendar: events + - googledrive: changes, file, file.update, file.trash, file.untrash, file.delete + - microsoft_teams: created, updated, deleted + - one_drive: updated + - outlook: created, updated, deleted + - share_point: updated + - slack: message, message.im, message.groups, message.channels, message.mpim, reaction_added, reaction_removed, member_joined_channel, member_left_channel, file_shared + - slackbot: app_mention, message.im, message.mpim, message.channels, message.groups + - wix: wix.forms.v4.submission_created, wix.contacts.v4.contact_created, wix.contacts.v4.contact_updated, wix.contacts.v4.contact_deleted, wix.ecom.v1.order_created, wix.ecom.v1.order_updated, wix.ecom.v1.order_canceled, wix.ecom.v1.checkout_created, wix.data.v2.data_item_created, wix.data.v2.data_item_updated, wix.data.v2.data_item_deleted ### in_app_agent ```json diff --git a/skills/base44-platform-workflows/references/web-tanstack.md b/skills/base44-platform-workflows/references/web-tanstack.md index 90f3b56..4ad1388 100644 --- a/skills/base44-platform-workflows/references/web-tanstack.md +++ b/skills/base44-platform-workflows/references/web-tanstack.md @@ -267,7 +267,7 @@ Optional: `resource_id` — for Google Calendar (calendar ID, e.g. "primary" or Prerequisites: - The connector must be authorized (use the Base44 MCP connector tools `list_connectors` and `initiate_connector_connection`, or `base44 connectors`). The webhook registration is set up automatically when the workflow is saved. -- Each provider has a different payload structure and auth model. Always read the connector's usage guide (`list_connectors` with the integration type) for the provider-specific webhook handler guide BEFORE writing the server route. +- Each provider has a different payload structure and auth model. Read the provider's own webhook documentation for its payload and auth model (`list_connectors` gives the integration type, scopes and connection state, not a handler guide) BEFORE writing the server route. Trigger payload: - `${ .trigger.integration_type }` — e.g. "slack", "googlecalendar" @@ -286,7 +286,16 @@ High-volume connectors (e.g. Slack) require conditions — validation will rejec If activation fails with `MISSING_WEBHOOK_SCOPES`, re-connect that connector (`initiate_connector_connection`, or `base44 connectors initiate`) with the union of its granted and missing scopes (re-authorization replaces the grant, so omitting the granted ones drops them), then save the workflow again. Available connectors: - (no connectors with webhook support) + - gmail: mailbox + - googlecalendar: events + - googledrive: changes, file, file.update, file.trash, file.untrash, file.delete + - microsoft_teams: created, updated, deleted + - one_drive: updated + - outlook: created, updated, deleted + - share_point: updated + - slack: message, message.im, message.groups, message.channels, message.mpim, reaction_added, reaction_removed, member_joined_channel, member_left_channel, file_shared + - slackbot: app_mention, message.im, message.mpim, message.channels, message.groups + - wix: wix.forms.v4.submission_created, wix.contacts.v4.contact_created, wix.contacts.v4.contact_updated, wix.contacts.v4.contact_deleted, wix.ecom.v1.order_created, wix.ecom.v1.order_updated, wix.ecom.v1.order_canceled, wix.ecom.v1.checkout_created, wix.data.v2.data_item_created, wix.data.v2.data_item_updated, wix.data.v2.data_item_deleted ### in_app_agent ```json From 8b7e14bc928078449441fc21ec8cc14992ae2995 Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Thu, 22 Oct 2026 12:58:59 +0000 Subject: [PATCH 09/15] sync: re-pin to source 0d4afb64f69 (main merged; tree unchanged or refreshed) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLKGecSgMqR14kYQTrugoR --- SOURCE_COMMIT | 2 +- .../base44-platform-integrations-sdk/references/mobile.md | 2 ++ .../base44-platform-integrations-sdk/references/web-spa.md | 2 ++ .../references/web-tanstack.md | 2 ++ skills/base44-platform-manifest.json | 6 +++--- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/SOURCE_COMMIT b/SOURCE_COMMIT index 090cd1a..19d0822 100644 --- a/SOURCE_COMMIT +++ b/SOURCE_COMMIT @@ -1 +1 @@ -99a6de6078ee0e55b90bdd18c99a71d9058faab4 +0d4afb64f69319c8c6fa4a8421fac5b6e98ba905 diff --git a/skills/base44-platform-integrations-sdk/references/mobile.md b/skills/base44-platform-integrations-sdk/references/mobile.md index 59d1946..12ca735 100644 --- a/skills/base44-platform-integrations-sdk/references/mobile.md +++ b/skills/base44-platform-integrations-sdk/references/mobile.md @@ -37,6 +37,8 @@ Default every end-user upload to `UploadPrivateFile`. Use `UploadPublicFile` onl `SendEmail` reaches registered app users normally. This app may ALSO email addresses that are not registered users, but only with a connected custom domain, on a paid plan, and subject to content screening, recipient-hygiene and anti-abuse limits that can refuse or pause sends — so never promise delivery to a non-registered address, and never route untrusted end-user input straight into the recipient. Invitations still use `base44.users.inviteUser`. +`SendEmail` accepts an optional `attachments` list: `[{filename, file_url}]` for a file this app uploaded, or `[{filename, content}]` with base64 bytes for a document the function built itself. Max 5 per send, 5 MB each, 10 MB total; documents and images only (pdf, png, jpg, gif, webp, csv, txt, md, ics, xlsx, docx) — archives and executables are refused server-side, as is a payload whose bytes don't match its extension. A `file_url` must be this app's own file. Never derive `attachments` from request input: a caller-chosen file plus a caller-chosen recipient mails the app's own data to a stranger. + Here are the integration packages available in this app: <integration_package> diff --git a/skills/base44-platform-integrations-sdk/references/web-spa.md b/skills/base44-platform-integrations-sdk/references/web-spa.md index 9e89681..aaf7801 100644 --- a/skills/base44-platform-integrations-sdk/references/web-spa.md +++ b/skills/base44-platform-integrations-sdk/references/web-spa.md @@ -32,6 +32,8 @@ InvokeLLM optional `model` (e.g. model: "claude-sonnet-5") for complex tasks — `SendEmail` reaches registered app users normally. This app may ALSO email addresses that are not registered users, but only with a connected custom domain, on a paid plan, and subject to content screening, recipient-hygiene and anti-abuse limits that can refuse or pause sends — so never promise delivery to a non-registered address, and never route untrusted end-user input straight into the recipient. Invitations still use `base44.users.inviteUser`. +`SendEmail` optional `attachments`: `[{filename, file_url}]` for a file this app uploaded, or `[{filename, content}]` with base64 for one the function built. Max 5 per send, 5 MB each, 10 MB total; documents and images only (pdf, png, jpg, gif, webp, csv, txt, md, ics, xlsx, docx) — archives and executables are refused server-side, as is a payload whose bytes don't match its extension. `file_url` must be this app's own file. Never derive `attachments` from request input: a caller-chosen file plus a caller-chosen recipient mails the app's own data to a stranger. + Integration packages in this app: <integration_package> diff --git a/skills/base44-platform-integrations-sdk/references/web-tanstack.md b/skills/base44-platform-integrations-sdk/references/web-tanstack.md index 87c1a93..475321f 100644 --- a/skills/base44-platform-integrations-sdk/references/web-tanstack.md +++ b/skills/base44-platform-integrations-sdk/references/web-tanstack.md @@ -30,6 +30,8 @@ InvokeLLM optional `model` (e.g. model: "claude-sonnet-5") for complex tasks — `SendEmail` reaches registered app users normally. This app may ALSO email addresses that are not registered users, but only with a connected custom domain, on a paid plan, and subject to content screening, recipient-hygiene and anti-abuse limits that can refuse or pause sends — so never promise delivery to a non-registered address, and never route untrusted end-user input straight into the recipient. Invitations still use `base44.users.inviteUser`. +`SendEmail` optional `attachments`: `[{filename, file_url}]` for a file this app uploaded, or `[{filename, content}]` with base64 for one the function built. Max 5 per send, 5 MB each, 10 MB total; documents and images only (pdf, png, jpg, gif, webp, csv, txt, md, ics, xlsx, docx) — archives and executables are refused server-side, as is a payload whose bytes don't match its extension. `file_url` must be this app's own file. Never derive `attachments` from request input: a caller-chosen file plus a caller-chosen recipient mails the app's own data to a stranger. + Integration packages in this app: <integration_package> diff --git a/skills/base44-platform-manifest.json b/skills/base44-platform-manifest.json index 5806a3b..c21953e 100644 --- a/skills/base44-platform-manifest.json +++ b/skills/base44-platform-manifest.json @@ -66,9 +66,9 @@ "skills/base44-platform-games/references/threejs/triggers_and_collisions.md": "f981ba3c28e7f65f11e9f01e082ba24490ddb7e0c5aa9ee961d008a421f47758", "skills/base44-platform-games/references/threejs/vfx_and_juice.md": "84e3e98d8b72fe01b73b9c1da4b3c77a38f85a0500a2ceb54f18b5fc4cf73b4c", "skills/base44-platform-integrations-sdk/SKILL.md": "eea0a0061264ed71408db5b38787556d892472a13f2995d8c70464942bc143c2", - "skills/base44-platform-integrations-sdk/references/mobile.md": "824d6a81dcfb5eff3dd51c48debf3324b5db805db21f44884b75e834abe1a599", - "skills/base44-platform-integrations-sdk/references/web-spa.md": "09d9fe218764e0f116dc725fb76cd54559dc5fdd34aa3cc0d14663757343d423", - "skills/base44-platform-integrations-sdk/references/web-tanstack.md": "bd1d6422c866290b2b2dc095bcd39e571f87c691d6150eac7bdb92cc15d4c6da", + "skills/base44-platform-integrations-sdk/references/mobile.md": "234428eb01d45bd67569840dd32404018f1b60efacddd98908798cc57e7cc843", + "skills/base44-platform-integrations-sdk/references/web-spa.md": "6f384fbd1ce1eb822fddd55503053af25d1ecce709ed3988ec45efbabba0d232", + "skills/base44-platform-integrations-sdk/references/web-tanstack.md": "c7baee1ab96aac7196753baa8106d597711662b6c87149a60412667b34b197e0", "skills/base44-platform-platform/SKILL.md": "a3d6c63e17a6227315c8351cd7756db6612ad5e7a26aee844773073ec68ffcaf", "skills/base44-platform-platform/references/game.md": "5693f5d4c492fa669d951e8f2e85e713cf100009f8db9a5a0aa1842a74e5efba", "skills/base44-platform-platform/references/mobile.md": "bc5116e7e7896f56908dd8e400cb7b7101f0ee53597725a0af614002b1ac669a", From a11ed335b8d1c81e6aff406a2c3e879aca1c915b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:01:12 +0000 Subject: [PATCH 10/15] docs: update README to match skills (skill table, install, or links) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 575e33b..0e481a1 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ npx skills add base44/skills --skill base44-remote-dev --skill base44-sandbox -- |-------|-------------| | [`base44-cli`](skills/base44-cli/SKILL.md) | Create and manage Base44 projects using the CLI. Handles resource configuration (entities, backend functions, realtime actors, AI agents), initialization, and deployment. | | [`base44-sdk`](skills/base44-sdk/SKILL.md) | Build apps using the Base44 JavaScript SDK. Communicate with remote resources like entities, backend functions, realtime actors, and AI agents. | -| [`base44-troubleshooter`](skills/base44-troubleshooter/SKILL.md) | Troubleshoot production issues using backend function logs. Use when investigating app errors or diagnosing production problems. | +| [`base44-troubleshooter`](skills/base44-troubleshooter/SKILL.md) | Troubleshoot production issues using backend function logs and workflow run history. Use when investigating app errors or diagnosing failed scheduled jobs. | | [`base44-remote-dev`](skills/base44-remote-dev/SKILL.md) | Develop a Base44 app remotely from your own coding agent by connecting it to the Base44 sandbox over MCP or the `base44 sandbox` CLI. | | [`base44-sandbox`](skills/base44-sandbox/SKILL.md) | Author Base44 app code inside the cloud sandbox — no deploy/push; writing a resource file (function, actor, entity, agent) into the sandbox is what ships it. | | [`base44-platform-platform`](skills/base44-platform-platform/SKILL.md) | Platform concepts for a Base44 app: entities, functions or server code, workflows, file structure, authentication and user invites, per app type. Compiled from the Base44 builder's own prompt. | @@ -78,7 +78,7 @@ npx skills add base44/skills --skill base44-remote-dev --skill base44-sandbox -- | [`base44-platform-integrations-sdk`](skills/base44-platform-integrations-sdk/SKILL.md) | Built-in integrations (InvokeLLM, SendEmail, file uploads, data extraction) with every Core endpoint schema, plus analytics. Compiled from the Base44 builder's own prompt. | | [`base44-platform-connectors`](skills/base44-platform-connectors/SKILL.md) | App connectors: OAuth integrations in shared, app-user and BYO-shared modes, and how server code reads their tokens. Compiled from the Base44 builder's own prompt. | | [`base44-platform-frontend`](skills/base44-platform-frontend/SKILL.md) | Frontend coding standards: conventions, installed packages, routing, Tailwind and design tokens, npm installs, per framework. Compiled from the Base44 builder's own prompt. | -| [`base44-platform-games`](skills/base44-platform-games/SKILL.md) | Phaser 4 and Three.js game references: architecture, assets, analytics and per-topic engine guides. Compiled from the Base44 builder's own prompt. | +| [`base44-platform-games`](skills/base44-platform-games/SKILL.md) | Phaser 4 and Three.js (via React Three Fiber) references: scenes, physics, input, cameras, assets and HUD. Compiled from the Base44 builder's own prompt. | ### Platform skills (compiled) From c5d11d331d14b57ab427723e120e824e4b50d24b Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Thu, 22 Oct 2026 14:07:16 +0000 Subject: [PATCH 11/15] sync: workflows reference serves mobile apps too (source be3787e1b25) Mobile apps on the workflows pipeline read references/backend-functions.md (renamed from web-spa.md); mobile-automations.md is for legacy-pipeline apps only. Rate-limit wording on the profile endpoint follows the enforced per-caller budget. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLKGecSgMqR14kYQTrugoR --- SOURCE_COMMIT | 2 +- skills/base44-platform-manifest.json | 6 +++--- skills/base44-platform-workflows/SKILL.md | 4 ++-- .../references/{web-spa.md => backend-functions.md} | 2 +- .../references/mobile-automations.md | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) rename skills/base44-platform-workflows/references/{web-spa.md => backend-functions.md} (99%) diff --git a/SOURCE_COMMIT b/SOURCE_COMMIT index 19d0822..e660e38 100644 --- a/SOURCE_COMMIT +++ b/SOURCE_COMMIT @@ -1 +1 @@ -0d4afb64f69319c8c6fa4a8421fac5b6e98ba905 +be3787e1b25d8688b81c83383d47a8bef76e16ef diff --git a/skills/base44-platform-manifest.json b/skills/base44-platform-manifest.json index c21953e..04e45dd 100644 --- a/skills/base44-platform-manifest.json +++ b/skills/base44-platform-manifest.json @@ -74,10 +74,10 @@ "skills/base44-platform-platform/references/mobile.md": "bc5116e7e7896f56908dd8e400cb7b7101f0ee53597725a0af614002b1ac669a", "skills/base44-platform-platform/references/web-spa.md": "52917f4b90d778d21e73b70ab7862e9250044831b51db4dd8723cf68a6706941", "skills/base44-platform-platform/references/web-tanstack.md": "625c2c6f75647c843292b9a864db8b737b674848cae1d9e44e9630185c8429d9", - "skills/base44-platform-workflows/SKILL.md": "c48b355ef3298ac81c662c4bb5bffb40486ede2ea42405d8ff00d3a01d46fae9", + "skills/base44-platform-workflows/SKILL.md": "59a92e17dd5d7c4a151a9733f1218749cce571b7101315a04b307446b74d0726", "skills/base44-platform-workflows/references/automations-legacy.md": "0518e4aaab82d2e2ff842fce5eb7daeb140900eb48a952a3528e7cc84e2db502", - "skills/base44-platform-workflows/references/mobile-automations.md": "c122edd2de022cd63bffa54272ffd32d0d893544cdf82db216ea8b39f15a0a15", - "skills/base44-platform-workflows/references/web-spa.md": "20666c624bcd735ff88e0fba5dd09ed98c68c80ff9e0edf50061d92d9fb3fc6e", + "skills/base44-platform-workflows/references/backend-functions.md": "92307752be105d6db7e13576740785281e1812dec508f66a2b530312973b71f3", + "skills/base44-platform-workflows/references/mobile-automations.md": "976a3adca97c0023352bccdefa36a6330a90e4db48d67aa3694f0dedf646de67", "skills/base44-platform-workflows/references/web-tanstack.md": "679d1a60b5854952426f08f71001dd8e962ffd1a15e2c514a2f88c4045b848f7" } } diff --git a/skills/base44-platform-workflows/SKILL.md b/skills/base44-platform-workflows/SKILL.md index d10d08f..605ecd9 100644 --- a/skills/base44-platform-workflows/SKILL.md +++ b/skills/base44-platform-workflows/SKILL.md @@ -21,6 +21,6 @@ native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwi | Profile | Read | |---|---| -| Web app, Vite SPA (the default web template) | [Workflows (Vite SPA: steps call backend functions)](references/web-spa.md), [Legacy automations (apps that predate workflows)](references/automations-legacy.md) | +| Web app, Vite SPA (the default web template) | [Workflows (Vite SPA and mobile: steps call backend functions)](references/backend-functions.md), [Legacy automations (apps that predate workflows)](references/automations-legacy.md) | | Web app, TanStack Start | [Workflows (TanStack Start: steps call server routes)](references/web-tanstack.md) | -| Native mobile app (Expo, React Native) | [Automations (mobile apps)](references/mobile-automations.md) | +| Native mobile app (Expo, React Native) | [Workflows (Vite SPA and mobile: steps call backend functions)](references/backend-functions.md), [Legacy automations (mobile apps that predate workflows)](references/mobile-automations.md) | diff --git a/skills/base44-platform-workflows/references/web-spa.md b/skills/base44-platform-workflows/references/backend-functions.md similarity index 99% rename from skills/base44-platform-workflows/references/web-spa.md rename to skills/base44-platform-workflows/references/backend-functions.md index 8239bd4..c0d4411 100644 --- a/skills/base44-platform-workflows/references/web-spa.md +++ b/skills/base44-platform-workflows/references/backend-functions.md @@ -1,7 +1,7 @@ <!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by the next sync; change the source instead. --> -# Base44 workflows: Workflows (Vite SPA: steps call backend functions) +# Base44 workflows: Workflows (Vite SPA and mobile: steps call backend functions) <workflows_capability> Workflows: trigger-driven automated processes — one step (e.g. a scheduled function call) or complex (multi-step, `switch` branching, durable waits across hours/days, data threading, chained backend functions). For any "when X happens, do Y" / "every N hours/days, do Y", however many actions Y involves. diff --git a/skills/base44-platform-workflows/references/mobile-automations.md b/skills/base44-platform-workflows/references/mobile-automations.md index 472d06e..c6bcd7d 100644 --- a/skills/base44-platform-workflows/references/mobile-automations.md +++ b/skills/base44-platform-workflows/references/mobile-automations.md @@ -1,7 +1,7 @@ <!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by the next sync; change the source instead. --> -# Base44 workflows: Automations (mobile apps) +# Base44 workflows: Legacy automations (mobile apps that predate workflows) <automations_capability> You can manage automations to run backend functions automatically - on a schedule, when entity records change, when external services send webhook events, or when app users start in-app agent conversations. From ba00093e4489a2d7be111ea3fdaf00018538d9b9 Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Thu, 22 Oct 2026 14:23:21 +0000 Subject: [PATCH 12/15] sync: re-pin to source aaecd19e505 (main merged, tree unchanged) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLKGecSgMqR14kYQTrugoR --- SOURCE_COMMIT | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SOURCE_COMMIT b/SOURCE_COMMIT index e660e38..0fb9feb 100644 --- a/SOURCE_COMMIT +++ b/SOURCE_COMMIT @@ -1 +1 @@ -be3787e1b25d8688b81c83383d47a8bef76e16ef +aaecd19e5051c29838576054b8914b1147dc9bc2 From 8e8e158817a2ad8d6ec2c8d8f93fc4a538604539 Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Sun, 25 Oct 2026 14:10:21 +0000 Subject: [PATCH 13/15] sync: drop the games skill; re-pin to source 35b6b8c21d6 Nine compiled skills now: a game app is a Vite SPA and reads the web-spa references. The entities reference for mobile is renamed mobile.md, the plugin manifests and docs no longer list base44-platform-games. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2fHMwsHuKC2HxRkojwH2X --- .claude-plugin/plugin.json | 3 +- .cursor-plugin/plugin.json | 3 +- CONTRIBUTING.md | 2 +- README.md | 3 +- SOURCE_COMMIT | 2 +- .../base44-sandbox/.claude-plugin/plugin.json | 3 +- .../base44-sandbox/.codex-plugin/plugin.json | 3 +- .../base44-sandbox/.cursor-plugin/plugin.json | 3 +- skills/base44-platform-actors/SKILL.md | 4 +- skills/base44-platform-agents/SKILL.md | 2 +- .../SKILL.md | 4 +- skills/base44-platform-connectors/SKILL.md | 2 +- skills/base44-platform-entities/SKILL.md | 6 +- .../{mobile-and-games.md => mobile.md} | 2 +- skills/base44-platform-frontend/SKILL.md | 4 +- skills/base44-platform-games/SKILL.md | 66 ---- .../references/game-2d.md | 329 ------------------ .../references/game-3d.md | 149 -------- .../phaser/actions_and_utilities.md | 118 ------- .../references/phaser/animations.md | 249 ------------- .../references/phaser/audio_and_sound.md | 229 ------------ .../references/phaser/cameras.md | 250 ------------- .../references/phaser/curves_and_paths.md | 182 ---------- .../references/phaser/data_manager.md | 236 ------------- .../references/phaser/events_system.md | 193 ---------- .../references/phaser/filters_and_postfx.md | 198 ----------- .../phaser/game_object_components.md | 158 --------- .../phaser/game_setup_and_config.md | 233 ------------- .../references/phaser/geometry_and_math.md | 156 --------- .../references/phaser/graphics_and_shapes.md | 229 ------------ .../phaser/groups_and_containers.md | 221 ------------ .../phaser/input_keyboard_mouse_touch.md | 310 ----------------- .../references/phaser/loading_assets.md | 265 -------------- .../references/phaser/mesh_and_3d_elements.md | 50 --- .../references/phaser/particles.md | 290 --------------- .../references/phaser/physics_arcade.md | 256 -------------- .../references/phaser/physics_matter.md | 323 ----------------- .../references/phaser/render_textures.md | 245 ------------- .../references/phaser/scale_and_responsive.md | 188 ---------- .../references/phaser/scenes.md | 274 --------------- .../references/phaser/sprites_and_images.md | 207 ----------- .../references/phaser/text_and_bitmaptext.md | 249 ------------- .../references/phaser/tilemaps.md | 218 ------------ .../references/phaser/time_and_timers.md | 239 ------------- .../references/phaser/tweens.md | 173 --------- .../threejs/assets_models_textures.md | 25 -- .../references/threejs/cameras.md | 27 -- .../threejs/character_controller.md | 25 -- .../threejs/enemies_and_movement.md | 31 -- .../references/threejs/game_state_and_flow.md | 36 -- .../references/threejs/input_and_controls.md | 28 -- .../references/threejs/performance.md | 33 -- .../references/threejs/physics_dynamics.md | 26 -- .../threejs/projectiles_and_pooling.md | 25 -- .../references/threejs/scene_and_rendering.md | 29 -- .../references/threejs/text_hud_ui.md | 29 -- .../threejs/triggers_and_collisions.md | 28 -- .../references/threejs/vfx_and_juice.md | 36 -- .../base44-platform-integrations-sdk/SKILL.md | 2 +- skills/base44-platform-manifest.json | 64 +--- skills/base44-platform-platform/SKILL.md | 4 +- .../references/game.md | 20 -- skills/base44-platform-workflows/SKILL.md | 2 +- 63 files changed, 29 insertions(+), 6970 deletions(-) rename skills/base44-platform-entities/references/{mobile-and-games.md => mobile.md} (98%) delete mode 100644 skills/base44-platform-games/SKILL.md delete mode 100644 skills/base44-platform-games/references/game-2d.md delete mode 100644 skills/base44-platform-games/references/game-3d.md delete mode 100644 skills/base44-platform-games/references/phaser/actions_and_utilities.md delete mode 100644 skills/base44-platform-games/references/phaser/animations.md delete mode 100644 skills/base44-platform-games/references/phaser/audio_and_sound.md delete mode 100644 skills/base44-platform-games/references/phaser/cameras.md delete mode 100644 skills/base44-platform-games/references/phaser/curves_and_paths.md delete mode 100644 skills/base44-platform-games/references/phaser/data_manager.md delete mode 100644 skills/base44-platform-games/references/phaser/events_system.md delete mode 100644 skills/base44-platform-games/references/phaser/filters_and_postfx.md delete mode 100644 skills/base44-platform-games/references/phaser/game_object_components.md delete mode 100644 skills/base44-platform-games/references/phaser/game_setup_and_config.md delete mode 100644 skills/base44-platform-games/references/phaser/geometry_and_math.md delete mode 100644 skills/base44-platform-games/references/phaser/graphics_and_shapes.md delete mode 100644 skills/base44-platform-games/references/phaser/groups_and_containers.md delete mode 100644 skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md delete mode 100644 skills/base44-platform-games/references/phaser/loading_assets.md delete mode 100644 skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md delete mode 100644 skills/base44-platform-games/references/phaser/particles.md delete mode 100644 skills/base44-platform-games/references/phaser/physics_arcade.md delete mode 100644 skills/base44-platform-games/references/phaser/physics_matter.md delete mode 100644 skills/base44-platform-games/references/phaser/render_textures.md delete mode 100644 skills/base44-platform-games/references/phaser/scale_and_responsive.md delete mode 100644 skills/base44-platform-games/references/phaser/scenes.md delete mode 100644 skills/base44-platform-games/references/phaser/sprites_and_images.md delete mode 100644 skills/base44-platform-games/references/phaser/text_and_bitmaptext.md delete mode 100644 skills/base44-platform-games/references/phaser/tilemaps.md delete mode 100644 skills/base44-platform-games/references/phaser/time_and_timers.md delete mode 100644 skills/base44-platform-games/references/phaser/tweens.md delete mode 100644 skills/base44-platform-games/references/threejs/assets_models_textures.md delete mode 100644 skills/base44-platform-games/references/threejs/cameras.md delete mode 100644 skills/base44-platform-games/references/threejs/character_controller.md delete mode 100644 skills/base44-platform-games/references/threejs/enemies_and_movement.md delete mode 100644 skills/base44-platform-games/references/threejs/game_state_and_flow.md delete mode 100644 skills/base44-platform-games/references/threejs/input_and_controls.md delete mode 100644 skills/base44-platform-games/references/threejs/performance.md delete mode 100644 skills/base44-platform-games/references/threejs/physics_dynamics.md delete mode 100644 skills/base44-platform-games/references/threejs/projectiles_and_pooling.md delete mode 100644 skills/base44-platform-games/references/threejs/scene_and_rendering.md delete mode 100644 skills/base44-platform-games/references/threejs/text_hud_ui.md delete mode 100644 skills/base44-platform-games/references/threejs/triggers_and_collisions.md delete mode 100644 skills/base44-platform-games/references/threejs/vfx_and_juice.md delete mode 100644 skills/base44-platform-platform/references/game.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index ec2496a..af7aa7c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -32,7 +32,6 @@ "./skills/base44-platform-actors/", "./skills/base44-platform-integrations-sdk/", "./skills/base44-platform-connectors/", - "./skills/base44-platform-frontend/", - "./skills/base44-platform-games/" + "./skills/base44-platform-frontend/" ] } diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 10578d0..1749c73 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -34,7 +34,6 @@ "./skills/base44-platform-actors/", "./skills/base44-platform-integrations-sdk/", "./skills/base44-platform-connectors/", - "./skills/base44-platform-frontend/", - "./skills/base44-platform-games/" + "./skills/base44-platform-frontend/" ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b5bfb38..b5748da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -154,7 +154,7 @@ matrix; a passage that must not be public is marked internal. The next sync carr ### Which reference an agent should read Each `base44-platform-*` skill's `SKILL.md` routes by app profile (Vite SPA, TanStack Start, native -mobile, 2D or 3D game). The platform answers that question directly: +mobile). The platform answers that question directly: `GET /api/apps/{app_id}/agent-profile` (public API, also reachable through the Base44 MCP `execute_api` tool) returns the app's shape, the workspace's entitlements and the exact reference paths to read. diff --git a/README.md b/README.md index 0e481a1..4470a40 100644 --- a/README.md +++ b/README.md @@ -78,11 +78,10 @@ npx skills add base44/skills --skill base44-remote-dev --skill base44-sandbox -- | [`base44-platform-integrations-sdk`](skills/base44-platform-integrations-sdk/SKILL.md) | Built-in integrations (InvokeLLM, SendEmail, file uploads, data extraction) with every Core endpoint schema, plus analytics. Compiled from the Base44 builder's own prompt. | | [`base44-platform-connectors`](skills/base44-platform-connectors/SKILL.md) | App connectors: OAuth integrations in shared, app-user and BYO-shared modes, and how server code reads their tokens. Compiled from the Base44 builder's own prompt. | | [`base44-platform-frontend`](skills/base44-platform-frontend/SKILL.md) | Frontend coding standards: conventions, installed packages, routing, Tailwind and design tokens, npm installs, per framework. Compiled from the Base44 builder's own prompt. | -| [`base44-platform-games`](skills/base44-platform-games/SKILL.md) | Phaser 4 and Three.js (via React Three Fiber) references: scenes, physics, input, cameras, assets and HUD. Compiled from the Base44 builder's own prompt. | ### Platform skills (compiled) -The `base44-platform-*` skills are generated from the Base44 builder's own system prompt and synced here by Base44, so they describe the platform exactly as the in-editor AI learns it: one reference per app shape (Vite SPA, TanStack Start, native mobile, 2D/3D game). Ask the platform which shape an app is with `GET /api/apps/{app_id}/agent-profile` (also reachable through the Base44 MCP `execute_api` tool); it returns the exact reference files to read. Do not edit these files by hand: the `SOURCE_COMMIT` file records the source commit they were compiled from, and the next sync overwrites them. How the pipeline works and how to change the content: [CONTRIBUTING.md](CONTRIBUTING.md#compiled-platform-skills-base44-platform-). +The `base44-platform-*` skills are generated from the Base44 builder's own system prompt and synced here by Base44, so they describe the platform exactly as the in-editor AI learns it: one reference per app shape (Vite SPA, TanStack Start, native mobile). Ask the platform which shape an app is with `GET /api/apps/{app_id}/agent-profile` (also reachable through the Base44 MCP `execute_api` tool); it returns the exact reference files to read. Do not edit these files by hand: the `SOURCE_COMMIT` file records the source commit they were compiled from, and the next sync overwrites them. How the pipeline works and how to change the content: [CONTRIBUTING.md](CONTRIBUTING.md#compiled-platform-skills-base44-platform-). ## About Agent Skills diff --git a/SOURCE_COMMIT b/SOURCE_COMMIT index 0fb9feb..6664880 100644 --- a/SOURCE_COMMIT +++ b/SOURCE_COMMIT @@ -1 +1 @@ -aaecd19e5051c29838576054b8914b1147dc9bc2 +35b6b8c21d65725471706a50c5533abb2ea319e4 diff --git a/plugins/base44-sandbox/.claude-plugin/plugin.json b/plugins/base44-sandbox/.claude-plugin/plugin.json index 973489b..5faf374 100644 --- a/plugins/base44-sandbox/.claude-plugin/plugin.json +++ b/plugins/base44-sandbox/.claude-plugin/plugin.json @@ -30,7 +30,6 @@ "./skills/base44-platform-actors/", "./skills/base44-platform-integrations-sdk/", "./skills/base44-platform-connectors/", - "./skills/base44-platform-frontend/", - "./skills/base44-platform-games/" + "./skills/base44-platform-frontend/" ] } diff --git a/plugins/base44-sandbox/.codex-plugin/plugin.json b/plugins/base44-sandbox/.codex-plugin/plugin.json index 21ab30f..b269aab 100644 --- a/plugins/base44-sandbox/.codex-plugin/plugin.json +++ b/plugins/base44-sandbox/.codex-plugin/plugin.json @@ -30,8 +30,7 @@ "./skills/base44-platform-actors/", "./skills/base44-platform-integrations-sdk/", "./skills/base44-platform-connectors/", - "./skills/base44-platform-frontend/", - "./skills/base44-platform-games/" + "./skills/base44-platform-frontend/" ], "interface": { "displayName": "Base44 Sandbox", diff --git a/plugins/base44-sandbox/.cursor-plugin/plugin.json b/plugins/base44-sandbox/.cursor-plugin/plugin.json index ff15fa1..034171e 100644 --- a/plugins/base44-sandbox/.cursor-plugin/plugin.json +++ b/plugins/base44-sandbox/.cursor-plugin/plugin.json @@ -32,7 +32,6 @@ "./skills/base44-platform-actors/", "./skills/base44-platform-integrations-sdk/", "./skills/base44-platform-connectors/", - "./skills/base44-platform-frontend/", - "./skills/base44-platform-games/" + "./skills/base44-platform-frontend/" ] } diff --git a/skills/base44-platform-actors/SKILL.md b/skills/base44-platform-actors/SKILL.md index c565c20..d528287 100644 --- a/skills/base44-platform-actors/SKILL.md +++ b/skills/base44-platform-actors/SKILL.md @@ -17,12 +17,10 @@ Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (ov tool, or with your Base44 CLI token); it returns the framework, app type, runtime, entitlements and the exact reference files to read. Without it, detect from the project files: `src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a -native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. +native mobile (Expo) app; otherwise it is a Vite SPA. | Profile | Read | |---|---| | Web app, Vite SPA (the default web template) | [Realtime actors (web apps)](references/web.md) | | Web app, TanStack Start | [Realtime actors (web apps)](references/web.md) | | Native mobile app (Expo, React Native) | [Realtime actors (mobile apps)](references/mobile.md) | -| Game, 2D (Phaser 4) | [Realtime actors (web apps)](references/web.md) | -| Game, 3D (Three.js) | [Realtime actors (web apps)](references/web.md) | diff --git a/skills/base44-platform-agents/SKILL.md b/skills/base44-platform-agents/SKILL.md index c269ebe..afd2e4e 100644 --- a/skills/base44-platform-agents/SKILL.md +++ b/skills/base44-platform-agents/SKILL.md @@ -15,7 +15,7 @@ Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (ov tool, or with your Base44 CLI token); it returns the framework, app type, runtime, entitlements and the exact reference files to read. Without it, detect from the project files: `src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a -native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. +native mobile (Expo) app; otherwise it is a Vite SPA. | Profile | Read | |---|---| diff --git a/skills/base44-platform-backend-functions/SKILL.md b/skills/base44-platform-backend-functions/SKILL.md index 52273a2..0485e27 100644 --- a/skills/base44-platform-backend-functions/SKILL.md +++ b/skills/base44-platform-backend-functions/SKILL.md @@ -17,12 +17,10 @@ Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (ov tool, or with your Base44 CLI token); it returns the framework, app type, runtime, entitlements and the exact reference files to read. Without it, detect from the project files: `src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a -native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. +native mobile (Expo) app; otherwise it is a Vite SPA. | Profile | Read | |---|---| | Web app, Vite SPA (the default web template) | [Backend functions (Vite SPA)](references/web-spa.md) | | Web app, TanStack Start | [Server functions and server routes (TanStack Start)](references/web-tanstack.md) | | Native mobile app (Expo, React Native) | [Backend functions (mobile apps)](references/mobile.md) | -| Game, 2D (Phaser 4) | [Backend functions (Vite SPA)](references/web-spa.md) | -| Game, 3D (Three.js) | [Backend functions (Vite SPA)](references/web-spa.md) | diff --git a/skills/base44-platform-connectors/SKILL.md b/skills/base44-platform-connectors/SKILL.md index 8063e02..aeaa20a 100644 --- a/skills/base44-platform-connectors/SKILL.md +++ b/skills/base44-platform-connectors/SKILL.md @@ -17,7 +17,7 @@ Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (ov tool, or with your Base44 CLI token); it returns the framework, app type, runtime, entitlements and the exact reference files to read. Without it, detect from the project files: `src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a -native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. +native mobile (Expo) app; otherwise it is a Vite SPA. | Profile | Read | |---|---| diff --git a/skills/base44-platform-entities/SKILL.md b/skills/base44-platform-entities/SKILL.md index ce3c0db..b48db68 100644 --- a/skills/base44-platform-entities/SKILL.md +++ b/skills/base44-platform-entities/SKILL.md @@ -15,7 +15,7 @@ Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (ov tool, or with your Base44 CLI token); it returns the framework, app type, runtime, entitlements and the exact reference files to read. Without it, detect from the project files: `src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a -native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. +native mobile (Expo) app; otherwise it is a Vite SPA. ## For every app @@ -25,6 +25,4 @@ native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwi |---|---| | Web app, Vite SPA (the default web template) | [Entities and the entity SDK (web apps)](references/web.md) | | Web app, TanStack Start | [Entities and the entity SDK (web apps)](references/web.md) | -| Native mobile app (Expo, React Native) | [Entities and the entity SDK (mobile and game apps)](references/mobile-and-games.md) | -| Game, 2D (Phaser 4) | [Entities and the entity SDK (mobile and game apps)](references/mobile-and-games.md) | -| Game, 3D (Three.js) | [Entities and the entity SDK (mobile and game apps)](references/mobile-and-games.md) | +| Native mobile app (Expo, React Native) | [Entities and the entity SDK (mobile apps)](references/mobile.md) | diff --git a/skills/base44-platform-entities/references/mobile-and-games.md b/skills/base44-platform-entities/references/mobile.md similarity index 98% rename from skills/base44-platform-entities/references/mobile-and-games.md rename to skills/base44-platform-entities/references/mobile.md index 290ee8a..faf507a 100644 --- a/skills/base44-platform-entities/references/mobile-and-games.md +++ b/skills/base44-platform-entities/references/mobile.md @@ -1,7 +1,7 @@ <!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by the next sync; change the source instead. --> -# Base44 entities: Entities and the entity SDK (mobile and game apps) +# Base44 entities: Entities and the entity SDK (mobile apps) Entities are defined as json schema objects that define the shape of the data that is stored in the app. diff --git a/skills/base44-platform-frontend/SKILL.md b/skills/base44-platform-frontend/SKILL.md index faede2f..62a9e92 100644 --- a/skills/base44-platform-frontend/SKILL.md +++ b/skills/base44-platform-frontend/SKILL.md @@ -15,12 +15,10 @@ Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (ov tool, or with your Base44 CLI token); it returns the framework, app type, runtime, entitlements and the exact reference files to read. Without it, detect from the project files: `src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a -native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. +native mobile (Expo) app; otherwise it is a Vite SPA. | Profile | Read | |---|---| | Web app, Vite SPA (the default web template) | [Web app, Vite SPA](references/web-spa.md) | | Web app, TanStack Start | [Web app, TanStack Start](references/web-tanstack.md) | | Native mobile app (Expo, React Native) | [Native mobile app (Expo, React Native, NativeWind)](references/mobile.md) | -| Game, 2D (Phaser 4) | [Web app, Vite SPA](references/web-spa.md) | -| Game, 3D (Three.js) | [Web app, Vite SPA](references/web-spa.md) | diff --git a/skills/base44-platform-games/SKILL.md b/skills/base44-platform-games/SKILL.md deleted file mode 100644 index 5a1d737..0000000 --- a/skills/base44-platform-games/SKILL.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: base44-platform-games -description: "Base44 games: Phaser 4 (2D) and Three.js via React Three Fiber (3D) reference material for game apps built on Base44. Use when implementing scenes, physics, input, cameras, assets, HUD or game state in a Base44 game." ---- -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games - -A Base44 game is a React app with the game layer under `src/game/`; React owns the screen-space overlay and the Base44 SDK, the engine owns gameplay. Each topic reference covers one engine area. - -## Pick the reference for this app - -Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (over the Base44 MCP `execute_api` -tool, or with your Base44 CLI token); it returns the framework, app type, runtime, -entitlements and the exact reference files to read. Without it, detect from the project files: -`src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a -native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. - -| Profile | Read | -|---|---| -| Game, 2D (Phaser 4) | [2D game (Phaser 4): architecture, assets, analytics and the Phaser reference](references/game-2d.md) | -| Game, 3D (Three.js) | [3D game (Three.js): architecture, assets, analytics and the Three.js reference](references/game-3d.md) | - -## Topic references - -- [Phaser 4: actions and utilities](references/phaser/actions_and_utilities.md) -- [Phaser 4: animations](references/phaser/animations.md) -- [Phaser 4: audio and sound](references/phaser/audio_and_sound.md) -- [Phaser 4: cameras](references/phaser/cameras.md) -- [Phaser 4: curves and paths](references/phaser/curves_and_paths.md) -- [Phaser 4: data manager](references/phaser/data_manager.md) -- [Phaser 4: events system](references/phaser/events_system.md) -- [Phaser 4: filters and postfx](references/phaser/filters_and_postfx.md) -- [Phaser 4: game object components](references/phaser/game_object_components.md) -- [Phaser 4: game setup and config](references/phaser/game_setup_and_config.md) -- [Phaser 4: geometry and math](references/phaser/geometry_and_math.md) -- [Phaser 4: graphics and shapes](references/phaser/graphics_and_shapes.md) -- [Phaser 4: groups and containers](references/phaser/groups_and_containers.md) -- [Phaser 4: input keyboard mouse touch](references/phaser/input_keyboard_mouse_touch.md) -- [Phaser 4: loading assets](references/phaser/loading_assets.md) -- [Phaser 4: mesh and 3d elements](references/phaser/mesh_and_3d_elements.md) -- [Phaser 4: particles](references/phaser/particles.md) -- [Phaser 4: physics arcade](references/phaser/physics_arcade.md) -- [Phaser 4: physics matter](references/phaser/physics_matter.md) -- [Phaser 4: render textures](references/phaser/render_textures.md) -- [Phaser 4: scale and responsive](references/phaser/scale_and_responsive.md) -- [Phaser 4: scenes](references/phaser/scenes.md) -- [Phaser 4: sprites and images](references/phaser/sprites_and_images.md) -- [Phaser 4: text and bitmaptext](references/phaser/text_and_bitmaptext.md) -- [Phaser 4: tilemaps](references/phaser/tilemaps.md) -- [Phaser 4: time and timers](references/phaser/time_and_timers.md) -- [Phaser 4: tweens](references/phaser/tweens.md) -- [Three.js: assets models textures](references/threejs/assets_models_textures.md) -- [Three.js: cameras](references/threejs/cameras.md) -- [Three.js: character controller](references/threejs/character_controller.md) -- [Three.js: enemies and movement](references/threejs/enemies_and_movement.md) -- [Three.js: game state and flow](references/threejs/game_state_and_flow.md) -- [Three.js: input and controls](references/threejs/input_and_controls.md) -- [Three.js: performance](references/threejs/performance.md) -- [Three.js: physics dynamics](references/threejs/physics_dynamics.md) -- [Three.js: projectiles and pooling](references/threejs/projectiles_and_pooling.md) -- [Three.js: scene and rendering](references/threejs/scene_and_rendering.md) -- [Three.js: text hud ui](references/threejs/text_hud_ui.md) -- [Three.js: triggers and collisions](references/threejs/triggers_and_collisions.md) -- [Three.js: vfx and juice](references/threejs/vfx_and_juice.md) diff --git a/skills/base44-platform-games/references/game-2d.md b/skills/base44-platform-games/references/game-2d.md deleted file mode 100644 index 3238a2c..0000000 --- a/skills/base44-platform-games/references/game-2d.md +++ /dev/null @@ -1,329 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: 2D game (Phaser 4): architecture, assets, analytics and the Phaser reference - -<game_architecture> -The game project is a standard React/Base44 app with a game layer under `src/game/`. - -<file_structure> -The game template ships this skeleton ALREADY WIRED: `src/App.jsx` routes `/` to GamePage, `GamePage.jsx` renders `<GameContainer />` (the engine-agnostic mount that owns the `#game-container` div), and `src/game/main.js` re-exports the active engine flavor. Build the game by EDITING these files in place — never recreate them or build a parallel structure elsewhere. The skeleton files are small and self-documenting: read them before changing them instead of relying on a from-memory picture of their contents. - -- `src/game/2d/` - ALL gameplay code for this game: scenes, game objects, systems, spawning, scoring, enemy AI. The starter scene is `scenes/GameScene.js` — gameplay code starts THERE; add further scene files under `scenes/` and reusable game-object files (e.g. `objects/`) alongside it. -- `src/game/main.js` - one-line engine switch re-exporting the active flavor's `Game` component. -- `src/game/config.js` - create for tunable game constants (see configuration). -- `src/game/EventBus.js` - engine-agnostic event emitter for React <-> game communication; rarely needs editing. -- `src/pages/GamePage.jsx` - React page that mounts the game; edit to build the HUD/menu overlay. -- `src/components/game/GameContainer.jsx` - mount wrapper; rarely needs editing. -- `src/App.jsx` - React router and Base44 shell; already mounts GamePage at `/` — edit only to add MORE routes. -- `src/main.jsx` - React bootstrap. Do not edit this for game routing. - -An older game may have a flatter layout — no `2d/`/`3d/` flavor split, a `PhaserGame`/`ThreeGame` mount instead of `GameContainer`. Follow the existing wiring in that case; the placement and boundary rules below still apply. - -Placement rule: ALL gameplay code lives under `src/game/`. `src/components/game/` holds ONLY the mount wrapper. React overlay UI components go in `src/components/` (or inline in GamePage.jsx) and contain no gameplay logic. -</file_structure> - -<boundary_rules> -- Put Base44 SDK calls, auth-aware data loading, navigation, and ALL screen-space UI — start/pause/settings menus, game-over and victory screens, score/lives/level readouts, buttons, dialogs, leaderboards, forms — in React files as a DOM overlay over the canvas (see screen_space_ui below). Do not build these inside the game world. -- Put gameplay state, physics, collisions, animation, game input, cameras, render-loop behavior, and world-anchored text (floating damage numbers, speech bubbles above characters, pickup labels) in `src/game`. React overlay state is a display copy fed by events — never the authoritative home for gameplay data. -- Use `src/game/EventBus.js` for ALL React <-> game communication, in both directions: the game emits gameplay events (score changed, game over) that React UI renders; React emits commands (restart, pause, resume) that the game acts on. -- Persist durable game data (high scores, saves, progress, leaderboards) through Base44 entities from React — never call the SDK inside the game loop or a scene; bridge gameplay events to React via `EventBus`. -- Preserve the `current-scene-ready` event pattern: the active scene emits it once it is mounted so `GameContainer` can track its handle. This event does NOT mean the first playable frame rendered and must not drive `window.game.ready`. -- When adding scenes, register them in the `scene` array of the Phaser config (`src/game/2d/main.js`). -</boundary_rules> - -<screen_space_ui> -Decision rule: if an element would look and behave correctly as a DOM element positioned over the canvas with CSS, build it in React (Tailwind + shadcn/ui). If it must track a world position, move with the camera, or update every frame in lockstep with rendering, build it in the game world. -- React overlay: menus, pause/game-over/victory screens, score-lives-level HUD readouts, buttons, settings, instructions, dialogs, leaderboards, forms. -- Game world: damage numbers, world-anchored labels and speech bubbles, and per-frame readouts tightly coupled to the render loop. - -Overlay structure: overlay elements are SIBLINGS of `<GameContainer />` inside the game frame wrapper (`.game-frame` is already `position: relative`). The overlay layer is `absolute inset-0 pointer-events-none` so gameplay clicks reach the canvas; interactive children re-enable `pointer-events-auto`: -```jsx -<div className="game-frame"> - <GameContainer /> - <div className="absolute inset-0 pointer-events-none"> - <div className="absolute top-3 left-3 text-white text-base sm:text-xl font-bold">Score: {score}</div> - {phase === 'game-over' && ( - <div className="absolute inset-0 grid place-items-center bg-black/60 pointer-events-auto"> - <Button onClick={() => EventBus.emit('restart-game')}>Play Again</Button> - </div> - )} - </div> -</div> -``` - -Two-way EventBus contract: -- Game -> React (state): emit on DISCRETE gameplay events only — `EventBus.emit('score-changed', score)` when the score actually changes, `EventBus.emit('game-over', { score })` once. NEVER emit from the per-frame update loop to drive React state; a value that changes every frame stays rendered in the game world, or its emit is throttled (e.g. every 100-250ms). -- Ticking overlay state (run timers, cooldown readouts — anything on an interval or throttled emit) lives in a LEAF component that owns its own state, never in the page component: overlay components are React ancestors of the game mount, so state there re-renders the Canvas/game subtree every tick and the whole scene reconciles at that rate. -- React -> Phaser (commands): the scene registers listeners in `create()` and MUST remove them on shutdown — EventBus outlives scenes, so leftover listeners stack up on every restart: -```js -// in GameScene.create() -const onRestart = () => this.scene.restart(); -const onPause = () => this.scene.pause(); -const onResume = () => this.scene.resume(); -EventBus.on('restart-game', onRestart); -EventBus.on('pause-game', onPause); -EventBus.on('resume-game', onResume); -this.events.once('shutdown', () => { - EventBus.off('restart-game', onRestart); - EventBus.off('pause-game', onPause); - EventBus.off('resume-game', onResume); -}); -``` -- React side: subscribe in `useEffect` with stable named handlers and matching `EventBus.off` cleanup. - -Overlay fonts: the template maps `font-display`, `font-heading`, `font-body`, and `font-mono` to `--font-*` CSS variables — to use a custom font, add `@import url('...')` at the very top of `src/index.css` (before `@tailwind base;`) and change the matching `--font-*` variable in `:root`. Never `@apply` a class that is not a built-in Tailwind utility or one mapped in `tailwind.config.js` — the build fails with "The `X` class does not exist". -Overlay color token syntax must match its consumer: `hsl(var(--token))` requires HSL channels such as `0 0% 100%`; an RGB tuple such as `248 250 252` requires `rgb(var(--token))`. Never put unitless RGB channels into an HSL token. -</screen_space_ui> - -<import_path_rules> -Import app and game modules with the `@/` alias rooted at `src/`. This is the style the skeleton files already use — mirror it in every file you create or edit. - -- Examples: `import { EventBus } from '@/game/EventBus';`, `import { GameScene } from '@/game/2d/scenes/GameScene';`, `import { GameContainer } from '@/components/game/GameContainer';`, `import { SHOP_ITEMS } from '@/game/config';`, `import { base44 } from '@/api/base44Client';`. -- A relative import (`./sibling`) is fine for a file in the same directory, but prefer `@/` for anything that crosses directories. -- Never import app modules with bare paths like `game/EventBus` or `components/game/GameContainer` — the build treats those as npm packages and fails to resolve them. -</import_path_rules> - -<configuration> -For non-trivial games, create `src/game/config.js` and keep tunable game constants there: -- dimensions, speeds, gravity, score values, colors, asset keys, art style, and perspective. -- Import constants from `src/game/config.js` into scenes and the engine setup. -- When the user asks to tune behavior ("faster", "more gravity", "larger world"), prefer editing config constants instead of scattering magic numbers. -</configuration> - -<testability> -- Every player- or AI-initiated gameplay capability must be callable independently of rendering and its physical input binding. Human-input and AI handlers are thin wrappers over the same gameplay functions. Represent sustained or analog controls with explicit control-state setters (press/release or normalized values); never add verification-only movement that bypasses the real update, timing, or physics path. Do not write the tests themselves -- Route all gameplay randomness through one seedable RNG (Phaser's game-config `seed` / `Phaser.Math.RND`). Vary the seed per run; a pinned seed is for reproducible verification. -- Expose and MAINTAIN the game API on `window.game`: `ready`, side-effect-free state getters (phase, score, key entities), and the capability/control functions required to exercise the primary playable loop. Initialize/reset `ready` to false before boot and restart; set it true only after required assets/async systems have finished and at least one playable frame rendered. Set readiness after preload/create completes and the following game render, not from `current-scene-ready` inside `create()`. Keep the API current as the game grows — a stale API is worse than none. -- HUD and menus stay in the React DOM overlay (see screen_space_ui) — that is the automated-verifiable surface. -</testability> -</game_architecture> - -<game_assets> -By default, build game visuals programmatically in code with Phaser's built-in primitives; for richer visuals than code shapes can produce (detailed characters, creatures, props, tiles, full backgrounds), generate images with the runtime `GenerateImage` integration or use assets the user provides, describing the project's `ART_STYLE` in every prompt so assets share one look, and store each image URL in `src/game/config.js` to load in `preload()` — reference the saved URL on every run rather than regenerating on each build. - -<programmatic_visuals> -Create visuals from code: -- Shapes: `this.add.rectangle(...)`, `this.add.circle(...)`, `this.add.triangle(...)`, `this.add.star(...)`. -- Graphics: `const g = this.add.graphics()` then `g.fillStyle(0x...)`, `g.fillRect(...)`, `g.fillCircle(...)`, `g.lineStyle(...)`, `g.strokePath(...)`. -- Reusable textures: draw with a Graphics object, bake it with `g.generateTexture('key', w, h)`, then use `'key'` in sprites, physics bodies, groups, particles, and animations. -- Text: `this.add.text(...)` (web/system fonts — no font files). -- Variety from a few textures: vary tint (`sprite.setTint(0x...)`), alpha, rotation, and scale. -</programmatic_visuals> - -<style_consistency> -Pick one cohesive look before drawing: a vibrant color palette and a shape language -(e.g. flat-geometric, neon, blocky), and keep it consistent across scenes. Store the -choices in `src/game/config.js` (e.g. `PALETTE`, `ART_STYLE`, `GAME_PERSPECTIVE`) when -that file exists or when you create it, and read from it everywhere. - -Perspective shapes how you draw and move things: -- `top-down` - camera looks straight down; flat/tiled ground, no horizon. -- `side-view` - side-scroller / platformer; a horizon and layered background are fine. -- `isometric` - 3/4 diamond-aligned view. -</style_consistency> - -<image_orientation> -A generated image is static — at runtime you can't tell which way it was drawn. So decide an object's facing -when you generate it, bake it into the image prompt, and write the movement code to match: say -"a fighter jet seen from above, nose pointing up", not just "a fighter jet". This matters for anything with a -heading (characters, vehicles, projectiles, creatures); backgrounds, tiling textures, and symmetric pickups -(coins, gems) don't need it. - -Choose the facing from how the object MOVES, not from what it is: -- Travels in a fixed direction without turning → draw it facing that direction. A vertical shooter's ship and - its bullets go up, so draw them pointing up; a runner's hero moves right, so draw it facing right. -- Turns to aim or steer (your code rotates it toward its target or velocity) → draw it facing right; the - rotation then needs no offset. E.g. a homing missile, a twin-stick player, a turret, a top-down car. -- Faces several ways (a walking character) → draw one facing and mirror it for the opposite side; generate - the other facings as separate images only if the game needs them. - -Enemies usually face the opposite way to the player, so they confront it: if the hero faces right, patrolling -enemies face left. (An enemy that chases by rotating toward the player is the "turns to aim" case above.) - -Examples by perspective: -- top-down: terrain and props seen from straight above; a shooter ship/asteroid reads from overhead (nose up); - top-down-RPG characters are drawn from a high angle so the face still shows, not a hat seen from above. -- side-view: side profiles on the ground — hero faces right, enemies face left, trees and buildings upright. -- isometric: drawn at the 3/4 iso angle, aligned to the diamond grid. -- UI/HUD icons: flat and front-on, no game perspective. -</image_orientation> - -<backgrounds> -Whether a background should REPEAT depends on whether the game SCROLLS — not on the -perspective. When generating or sourcing one, pick the matching -kind and wire the result accordingly: - -- Scrolling games (camera moves through a world larger than the screen — a platformer - level, a scrolling overworld): a large image with seamless edges, tiled with - `this.add.tileSprite(...)` and scrolled via `tilePosition*` with the camera. Top-down - tiles on both axes; side-view tiles horizontally only (a horizon must not repeat - vertically — stretch it to fill the height with `tileScaleY`). - -- Static, single-screen games (a board, an arena, a menu, a one-screen puzzle): do NOT - tile. Use a large single image that COVERS the viewport — one `this.add.image(...)` - scaled by `Math.max(GAME_WIDTH / w, GAME_HEIGHT / h)` (preserve aspect, crop overflow), - or `setDisplaySize(GAME_WIDTH, GAME_HEIGHT)` to stretch. - -Simple backgrounds can still be drawn programmatically — a solid fill, a Graphics -gradient, or shapes — matching the chosen perspective. -</backgrounds> - -Do not mix inconsistent visual styles in the same scene unless the user explicitly wants that contrast. -</game_assets> - -To opt-out, set `{"analytics":{"enabled":false}}` when initializing the base44 SDK (supported from 0.8.46). - -<phaser_reference> -Deep Phaser reference sections (physics, particles, tilemaps, etc.) are the files under `references/phaser/`; read the one for the mechanic you are building before writing it. - -<scene_basics> -Every Phaser scene must import Phaser or the Phaser classes it extends/uses. - -Valid patterns: -```js -import Phaser from 'phaser'; -export class GameScene extends Phaser.Scene {} -``` - -```js -import { Scene } from 'phaser'; -export class GameScene extends Scene {} -``` - -Scene lifecycle: -- `preload()` loads assets. -- `create()` creates objects, physics, input handlers, cameras, and emits readiness events. -- `update(time, delta)` runs every frame. -</scene_basics> - -<arcade_physics_default> -Default to Arcade physics for top-down games, platformers, runners, shooters, and most action games. - -Common Arcade patterns: -```js -this.physics.add.existing(sprite); -sprite.body.setVelocityX(200); -sprite.body.setGravityY(300); -sprite.body.setCollideWorldBounds(true); - -const platforms = this.physics.add.staticGroup(); -this.physics.add.collider(player, platforms); -this.physics.add.overlap(player, coins, collectCoin, null, this); -``` -</arcade_physics_default> - -<input> -Support keyboard and pointer/touch where reasonable. - -```js -this.cursors = this.input.keyboard.createCursorKeys(); -this.input.on('pointerdown', (pointer) => {}); -``` -</input> - -<cameras> -```js -this.cameras.main.startFollow(player); -this.cameras.main.setBounds(0, 0, worldWidth, worldHeight); -this.cameras.main.setZoom(1.5); -``` -</cameras> - -<animation> -```js -this.anims.create({ - key: 'walk', - frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }), - frameRate: 10, - repeat: -1, -}); -sprite.play('walk'); -``` -</animation> - -<tweens_timers_and_scenes> -```js -this.tweens.add({ targets: sprite, y: sprite.y - 50, duration: 500, yoyo: true }); -this.time.addEvent({ delay: 1000, callback: spawnEnemy, loop: true }); -this.scene.start('NextScene'); -this.scene.launch('EffectsScene'); // parallel world-layer scene — screen-space UI lives in the React overlay, not a Phaser scene -this.registry.set('score', 0); -this.registry.get('score'); -``` -</tweens_timers_and_scenes> - -<fallback_textures> -If an asset has not loaded yet, the game should still run. Create fallback textures before using keys: -```js -createFallbackTexture(key, width, height, color) { - if (this.textures.exists(key)) return; - const graphics = this.add.graphics(); - graphics.fillStyle(color, 1); - graphics.fillRect(0, 0, width, height); - graphics.generateTexture(key, width, height); - graphics.destroy(); -} -``` -</fallback_textures> - -<sprite_sheets> -Load sprite sheets with frame dimensions. Never load a sprite sheet as a single image. - -```js -this.load.spritesheet('player', '/assets/game/player.png', { - frameWidth: 256, - frameHeight: 256, -}); - -this.player = this.physics.add.sprite(400, 300, 'player', 0); -``` - -The sprite displays one frame at a time. Phaser slices the grid into frames. - -Common mistakes to avoid: -- Do not use `this.load.image()` for sprite sheets. -- Do not omit `{ frameWidth, frameHeight }`. -- Do not forget an initial frame when creating the sprite. -- Stop walk animations when idle, otherwise characters keep walking in place. -- Use `setFlipX` when reusing a side-facing row for left/right movement. -</sprite_sheets> - -<audio> -Browsers block autoplay. Start or resume audio after a user click/tap. -</audio> -</phaser_reference> - -<phaser_v4_guide> -This project uses Phaser 4 (the template ships `phaser@^4.1.0`). Several common Phaser 3 APIs were removed or changed in v4 — always use the v4 forms below, not the older v3 equivalents. These rules apply to every change; deeper per-topic detail is in the files under `references/phaser/`. - -<v3_apis_removed_use_v4> -These Phaser 3 APIs are GONE in v4 — calling them throws or silently no-ops. Use the v4 form: - -- Mesh / Plane game objects are removed. There is NO `this.add.mesh()` or `this.add.plane()`, and `Phaser.Geom.Mesh` perspective helpers (`GenerateGridVerts`, `panZ`, `modelRotation`, `hideCCW`) are gone. For a 3D LOOK in a 2D Phaser game use pseudo-3D (depth scaling, parallax, isometric sorting) — see mesh_and_3d_elements. -- `setTintFill(color)` is removed → `sprite.setTint(color).setTintMode(Phaser.TintModes.FILL)`. Tint mode is now separate from tint color (modes: `MULTIPLY` (default), `FILL`, `ADD`, `SCREEN`, `OVERLAY`, `HARD_LIGHT`). -- `preFX` / `postFX` are removed → the Filters system. Call `sprite.enableFilters()` first (cameras already have filters), then add via `sprite.filters.internal.addX()` (object-local, cheaper) or `sprite.filters.external.addX()` (screen-space). e.g. `sprite.preFX.addGlow()` becomes `sprite.enableFilters(); sprite.filters.internal.addGlow()`. -- `setPipeline('Light2D')` is removed → `sprite.setLighting(true)` (per-object Lighting component; optional `setSelfShadow(...)`). Custom WebGL `Pipeline`s are gone too (replaced internally by RenderNodes) — do not hand-write a v3 pipeline; use Filters for post-processing. -- WebGL masks: `new Phaser.Display.Masks.BitmapMask(...)` + `setMask()` are removed for WebGL → `sprite.enableFilters(); sprite.filters.internal.addMask(maskTextureOrObject)`. (GeometryMask survives on the Canvas renderer only.) -- `Phaser.Geom.Point` is removed → use `Phaser.Math.Vector2` or a plain `{ x, y }` object. -- `Phaser.Structs.Set` is removed → use native JS `Set` (`add`/`has`/`delete`/`forEach`). (`Phaser.Structs.Map` still exists in v4, but native `Map` is fine for plain key/value storage.) -- `Phaser.Math.PI2` is removed → `Phaser.Math.TAU` (NOTE: in v4 `TAU` equals 2π, not π/2; for π/2 use `Phaser.Math.PI_OVER_2`). -- `RenderTexture` / `DynamicTexture` drawing is now buffered — `draw`/`stamp`/`fill`/`erase` push commands and you MUST call `.render()` to flush them (or set `renderMode: 'all'`); they no longer paint immediately. -- There is no dedicated Bloom FX → use `Phaser.Actions.AddEffectBloom(...)` or ParallelFilters (Threshold + Blur + `ADD` blend). -- `Phaser.Create.GenerateTexture` and the Create palettes are removed → build textures with a Graphics object + `generateTexture(key, w, h)` (the Graphics method still exists; the Create utility does not). -- `roundPixels` now defaults to `false` (v3 defaulted to `true`) — set it explicitly in the game/camera config when you need crisp pixel art. - -When tempted to reach for a v3-era API not listed here, prefer the documented v4 component/method and do not assume a v3 signature. -</v3_apis_removed_use_v4> - -<v4_capabilities_to_use> -v4 adds GPU-accelerated systems worth reaching for. One-liners below; full usage is in the matching `references/phaser/` file: -- Filters (`filters.internal` / `filters.external` after `enableFilters()`): modern post-processing on any object or camera — glow, blur, bloom, color matrix, displacement, vignette, masks. See filters_and_postfx. -- Lighting + self-shadow (`setLighting(true)`, `setSelfShadow(...)`): cheap 2D lighting with no pipeline. See game_object_components. -- New tint modes (`setTintMode(Phaser.TintModes.ADD | SCREEN | OVERLAY | HARD_LIGHT)`): additive glows and blends without filters. -- SpriteGPULayer (`this.add.spriteGPULayer(...)`): renders huge numbers of quads (up to millions) in a single draw call — reach for it on bullet-hell, dense particles-as-sprites, forests, starfields. See sprites_and_images. -- TilemapGPULayer (create the layer with the GPU option enabled): GPU tilemap whose cost is per-pixel, not per-tile, with no seams in LINEAR filtering. See tilemaps. -- Gradient and Noise game objects (`this.add.gradient(...)`, `this.add.noiseSimplex2D(...)`, etc.): GPU-generated gradients and procedural noise for skies, fog, water, and fire. See graphics_and_shapes / render_textures. -- CaptureFrame (`this.add.captureFrame(key)`): snapshot the framebuffer at a point in the display list into a texture, for echo/feedback effects. -</v4_capabilities_to_use> -</phaser_v4_guide> diff --git a/skills/base44-platform-games/references/game-3d.md b/skills/base44-platform-games/references/game-3d.md deleted file mode 100644 index 4d1cfa6..0000000 --- a/skills/base44-platform-games/references/game-3d.md +++ /dev/null @@ -1,149 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: 3D game (Three.js): architecture, assets, analytics and the Three.js reference - -<game_architecture> -The game project is a standard React/Base44 app with a game layer under `src/game/`. - -<file_structure> -The game template ships this skeleton ALREADY WIRED: `src/App.jsx` routes `/` to GamePage, `GamePage.jsx` renders `<GameContainer />` (the engine-agnostic mount that owns the `#game-container` div), and `src/game/main.js` re-exports the active engine flavor. Build the game by EDITING these files in place — never recreate them or build a parallel structure elsewhere. The skeleton files are small and self-documenting: read them before changing them instead of relying on a from-memory picture of their contents. - -- `src/game/3d/` - ALL gameplay code for this game: scenes, game objects, systems, spawning, scoring, enemy AI. The starter scene is `scenes/GameScene.jsx` — gameplay code starts THERE; add further scene files under `scenes/` and reusable game-object files (e.g. `objects/`) alongside it. -- `src/game/main.js` - one-line engine switch re-exporting the active flavor's `Game` component; must point at `@/game/3d/Game` for this game. -- `src/game/config.js` - create for tunable game constants (see configuration). -- `src/game/EventBus.js` - engine-agnostic event emitter for React <-> game communication; rarely needs editing. -- `src/pages/GamePage.jsx` - React page that mounts the game; edit to build the HUD/menu overlay. -- `src/components/game/GameContainer.jsx` - mount wrapper; rarely needs editing. -- `src/App.jsx` - React router and Base44 shell; already mounts GamePage at `/` — edit only to add MORE routes. -- `src/main.jsx` - React bootstrap. Do not edit this for game routing. - -An older game may have a flatter layout — no `2d/`/`3d/` flavor split, a `PhaserGame`/`ThreeGame` mount instead of `GameContainer`. Follow the existing wiring in that case; the placement and boundary rules below still apply. - -Placement rule: ALL gameplay code lives under `src/game/`. `src/components/game/` holds ONLY the mount wrapper. React overlay UI components go in `src/components/` (or inline in GamePage.jsx) and contain no gameplay logic. -</file_structure> - -<boundary_rules> -- Put Base44 SDK calls, auth-aware data loading, navigation, and ALL screen-space UI — start/pause/settings menus, game-over and victory screens, score/lives/level readouts, buttons, dialogs, leaderboards, forms — in React files as a DOM overlay over the canvas (see screen_space_ui below). Do not build these inside the game world. -- Put gameplay state, physics, collisions, animation, game input, cameras, render-loop behavior, and world-anchored text (floating damage numbers, speech bubbles above characters, pickup labels) in `src/game`. React overlay state is a display copy fed by events — never the authoritative home for gameplay data. -- Use `src/game/EventBus.js` for ALL React <-> game communication, in both directions: the game emits gameplay events (score changed, game over) that React UI renders; React emits commands (restart, pause, resume) that the game acts on. -- Persist durable game data (high scores, saves, progress, leaderboards) through Base44 entities from React — never call the SDK inside the game loop or a scene; bridge gameplay events to React via `EventBus`. -- Preserve the `current-scene-ready` event pattern: the active scene emits it once it is mounted so `GameContainer` can track its handle. This event does NOT mean the first playable frame rendered and must not drive `window.game.ready`. - -</boundary_rules> - -<screen_space_ui> -Decision rule: if an element would look and behave correctly as a DOM element positioned over the canvas with CSS, build it in React (Tailwind + shadcn/ui). If it must track a world position, move with the camera, or update every frame in lockstep with rendering, build it in the game world. -- React overlay: menus, pause/game-over/victory screens, score-lives-level HUD readouts, buttons, settings, instructions, dialogs, leaderboards, forms. -- Game world: damage numbers, world-anchored labels and speech bubbles, and per-frame readouts tightly coupled to the render loop. - -Overlay structure: overlay elements are SIBLINGS of `<GameContainer />` inside the game frame wrapper (`.game-frame` is already `position: relative`). The overlay layer is `absolute inset-0 pointer-events-none` so gameplay clicks reach the canvas; interactive children re-enable `pointer-events-auto`: -```jsx -<div className="game-frame"> - <GameContainer /> - <div className="absolute inset-0 pointer-events-none"> - <div className="absolute top-3 left-3 text-white text-base sm:text-xl font-bold">Score: {score}</div> - {phase === 'game-over' && ( - <div className="absolute inset-0 grid place-items-center bg-black/60 pointer-events-auto"> - <Button onClick={() => EventBus.emit('restart-game')}>Play Again</Button> - </div> - )} - </div> -</div> -``` - -Two-way EventBus contract: -- Game -> React (state): emit on DISCRETE gameplay events only — `EventBus.emit('score-changed', score)` when the score actually changes, `EventBus.emit('game-over', { score })` once. NEVER emit from the per-frame update loop to drive React state; a value that changes every frame stays rendered in the game world, or its emit is throttled (e.g. every 100-250ms). -- Ticking overlay state (run timers, cooldown readouts — anything on an interval or throttled emit) lives in a LEAF component that owns its own state, never in the page component: overlay components are React ancestors of the game mount, so state there re-renders the Canvas/game subtree every tick and the whole scene reconciles at that rate. -- React -> Game (commands): the scene/game component registers listeners in a `useEffect` and removes them in its cleanup — EventBus outlives remounts, so leftover listeners stack up. -- React side: subscribe in `useEffect` with stable named handlers and matching `EventBus.off` cleanup. - -Overlay fonts: the template maps `font-display`, `font-heading`, `font-body`, and `font-mono` to `--font-*` CSS variables — to use a custom font, add `@import url('...')` at the very top of `src/index.css` (before `@tailwind base;`) and change the matching `--font-*` variable in `:root`. Never `@apply` a class that is not a built-in Tailwind utility or one mapped in `tailwind.config.js` — the build fails with "The `X` class does not exist". -Overlay color token syntax must match its consumer: `hsl(var(--token))` requires HSL channels such as `0 0% 100%`; an RGB tuple such as `248 250 252` requires `rgb(var(--token))`. Never put unitless RGB channels into an HSL token. -</screen_space_ui> - -<import_path_rules> -Import app and game modules with the `@/` alias rooted at `src/`. This is the style the skeleton files already use — mirror it in every file you create or edit. - -- Examples: `import { EventBus } from '@/game/EventBus';`, `import { GameScene } from '@/game/3d/scenes/GameScene';`, `import { GameContainer } from '@/components/game/GameContainer';`, `import { SHOP_ITEMS } from '@/game/config';`, `import { base44 } from '@/api/base44Client';`. -- A relative import (`./sibling`) is fine for a file in the same directory, but prefer `@/` for anything that crosses directories. -- Never import app modules with bare paths like `game/EventBus` or `components/game/GameContainer` — the build treats those as npm packages and fails to resolve them. -</import_path_rules> - -<configuration> -For non-trivial games, create `src/game/config.js` and keep tunable game constants there: -- dimensions, speeds, gravity, score values, colors, asset keys, art style, and perspective. -- Import constants from `src/game/config.js` into scenes and the engine setup. -- When the user asks to tune behavior ("faster", "more gravity", "larger world"), prefer editing config constants instead of scattering magic numbers. -</configuration> - -<testability> -- Every player- or AI-initiated gameplay capability must be callable independently of rendering and its physical input binding. Human-input and AI handlers are thin wrappers over the same gameplay functions. Represent sustained or analog controls with explicit control-state setters (press/release or normalized values); never add verification-only movement that bypasses the real update, timing, or physics path. Do not write the tests themselves -- Route all gameplay randomness through one seedable RNG (a small seeded generator in `src/game`). Vary the seed per run; a pinned seed is for reproducible verification. -- Expose and MAINTAIN the game API on `window.game`: `ready`, side-effect-free state getters (phase, score, key entities), and the capability/control functions required to exercise the primary playable loop. Initialize/reset `ready` to false before boot and restart; set it true only after required assets/async systems have finished and at least one playable frame rendered. The readiness signal belongs inside the final Suspense/physics boundary and completes after that world participates in its first render frame. Keep the API current as the game grows — a stale API is worse than none. -- HUD and menus stay in the React DOM overlay (see screen_space_ui) — that is the automated-verifiable surface. -</testability> -</game_architecture> - -<game_assets> -By default, build game visuals programmatically in code with Three.js meshes, materials, and lights; for richer visuals than code shapes can produce (detailed characters, creatures, props, tiles, full backgrounds), generate images with the runtime `GenerateImage` integration or use assets the user provides, describing the project's `ART_STYLE` in every prompt so assets share one look, and store each image URL in `src/game/config.js` to load as textures — reference the saved URL on every run rather than regenerating on each build. - -Composing the 3D world itself (geometry, materials, lighting, procedural textures) is covered in the Three.js reference. - -<style_consistency> -Pick one cohesive look before building: a vibrant color palette and a shape language -(e.g. neon, blocky), and keep it consistent across the whole world. Store the -choices in `src/game/config.js` (e.g. `PALETTE`, `ART_STYLE`, `CAMERA_MODE`) when -that file exists or when you create it, and read from it everywhere. - -The camera mode shapes how you build and light the world — first-person, third-person -follow, orbit, or a fixed angle. Settle it up front and keep it consistent. -</style_consistency> - -Do not mix inconsistent visual styles in the same scene unless the user explicitly wants that contrast. -</game_assets> - -To opt-out, set `{"analytics":{"enabled":false}}` when initializing the base44 SDK (supported from 0.8.46). - -<threejs_reference> -This game is 3D: the world runs on Three.js through React Three Fiber (R3F) — there is NO Phaser in this game. The stack: -- `three` — the engine (always installed). -- `@react-three/fiber` — renders the Three.js scene graph as React components and owns the canvas and render loop. -- `@react-three/drei` — ready-made helpers (controls, cameras, loaders, text, sky, ...). Reach for drei before hand-rolling a helper. -- `@react-three/rapier` — physics (`<Physics>`, `<RigidBody>`, colliders). - -<dependencies> -Never install or re-pin a package already in `package.json`, and never import a package that is not in it. -</dependencies> - -<legacy_3d_games> -An older 3D game may mount Three.js manually — a component owning a `WebGLRenderer` and a `requestAnimationFrame` loop — instead of R3F. Follow that existing structure when editing such a game; do not rewrite it onto R3F unless the user asks. -</legacy_3d_games> - -<r3f_essentials> -- Scene content is JSX: `<mesh position={[0, 1, 0]}><boxGeometry args={[1, 1, 1]} /><meshStandardMaterial color="#38bdf8" /></mesh>`. `args` are constructor arguments; nested properties use dashed "pierced" props (`shadow-mapSize`, `shadow-camera-left`, `position-x`). -- Per-frame gameplay runs in `useFrame((state, delta) => ...)` inside components under the Canvas. Scale all movement by `delta` (seconds) so speed is frame-rate independent. -- NEVER set React state per frame, and NEVER hold live gameplay data — entity arrays (enemies, projectiles), hp, cooldowns — in useState: `useFrame` closures capture the render-time snapshot, so systems iterate a stale list and hits/targeting silently break. Mutate refs and shared module objects in `useFrame` (`meshRef.current.position.x += speed * delta`); React state only mirrors discrete events (phase changes, score). -- `useThree` gives the root state (scene, camera, gl, size). R3F hooks (`useFrame`, `useThree`, `useTexture`) only work in components rendered inside the Canvas — the React overlay outside cannot use them. -- Anything that loads asynchronously (textures, models) suspends — wrap it in `<Suspense fallback={null}>`. -- Structure the world as small components under `src/game/3d/` (Player, Enemies, Level, Effects) composed in the scene; keep tunables in `src/game/config.js`. -</r3f_essentials> - -<coordinates> -- The three.js frame: +X right, +Y up, and a camera looks down its −Z axis — "forward" / into the screen is −Z; +Z points at the viewer. -- The control convention is DERIVED from the camera rig and control scheme settled for this game, never assumed — record it once in `src/game/config.js` as signed constants the gameplay formulas import (forward/flow sign, spawn distance, camera offset), not just a `CAMERA_MODE` label: labels document, only consumed numbers keep files in agreement. A file that invents its own sign inline instead of importing it is how one subsystem ships mirrored against the rest. Chase/first-person (camera yaw follows the avatar): avatar faces −Z, camera behind at a positive local-Z offset, input camera-relative. Fixed rig (camera yaw never changes — top-down, isometric, side-view, lane runner): keys map to the camera's screen axes projected onto the ground, fixed world-direction constants. Aim/pointer games (pool, golf, board/puzzle): there may be no movement keys at all — don't bolt WASD on. -- Every system must derive from that one convention — input signs, model orientation, camera offset, spawn/flow direction, cull windows. A single flipped sign in one file reads as mirrored controls or a world scrolling backwards; fix a wrong direction at its source instead of negating it downstream. -</coordinates> - -<physics_rapier> -Use rapier when the game needs physical simulation — gravity, stacking, bouncing, knock-back, vehicles. Grid/board/puzzle games and simple arcade motion move meshes directly in `useFrame` without `<Physics>`. Wrap the physical world in `<Physics>` inside `<Suspense>` (the rapier WASM module initializes asynchronously); bodies, colliders, forces, and collision/trigger events are covered in the injected `physics_dynamics` and `triggers_and_collisions` reference sections. -</physics_rapier> - -<drei_helpers> -Common picks: `OrbitControls` (add `makeDefault`), `PerspectiveCamera`/`OrthographicCamera`, `KeyboardControls`, `Text`/`Billboard`, `Sky`, `Stars`, `Environment`, `useTexture`, `Html` — each is detailed in its injected reference section. -</drei_helpers> - -<visuals> -Compose visuals from primitives (`boxGeometry`, `sphereGeometry`, `cylinderGeometry`, `coneGeometry`, `planeGeometry`, `torusGeometry`, `capsuleGeometry`) inside `<group>`s for characters and props; share geometries/materials and vary color, scale, and rotation per instance. Image-tool URLs load as textures via `useTexture(url)` — art direction, palette, and asset rules are in the game assets guidance; lighting, shadows, and atmosphere in the injected `scene_and_rendering` reference section. -</visuals> -</threejs_reference> diff --git a/skills/base44-platform-games/references/phaser/actions_and_utilities.md b/skills/base44-platform-games/references/phaser/actions_and_utilities.md deleted file mode 100644 index 9a0d56e..0000000 --- a/skills/base44-platform-games/references/phaser/actions_and_utilities.md +++ /dev/null @@ -1,118 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: actions and utilities - -<actions_and_utilities> -Use this when you need batch operations on arrays of Game Objects, alignment and grid layout, or the Phaser.Utils array/object/string helper functions. - -<quick_start> -`Phaser.Actions` runs batch operations over arrays of Game Objects. The first argument is always an array, every action returns the same array, and Groups work by passing `group.getChildren()`. - -```js -// Create 20 sprites and batch-position them in a grid -const sprites = []; -for (let i = 0; i < 20; i++) { - sprites.push(this.add.sprite(0, 0, 'gem')); -} - -// Arrange into a 5x4 grid -Phaser.Actions.GridAlign(sprites, { - width: 5, - height: 4, - cellWidth: 64, - cellHeight: 64, - x: 100, - y: 100 -}); - -// Fade alpha from 0 to 1 across all sprites -Phaser.Actions.Spread(sprites, 'alpha', 0, 1); - -// Offset each sprite's x by 10, with a step of 2 per item -Phaser.Actions.IncX(sprites, 10, 2); - -// Works with Groups too -const group = this.add.group({ key: 'star', repeat: 11 }); -Phaser.Actions.PlaceOnCircle(group.getChildren(), new Phaser.Geom.Circle(400, 300, 200)); -``` -</quick_start> - -<core_concepts> -Every Action in `Phaser.Actions` follows the same shape: the first argument is an array of Game Objects (or any objects with the required public properties like `x`, `y`, `alpha`); it returns the same array for chaining/pass-through; it works with Groups via `group.getChildren()`; and it is a one-shot batch operation that stores no state. - -Most Set/Inc actions delegate to two core functions. `PropertyValueSet(items, key, value, step, index, direction)` sets `items[i][key] = value + (i * step)`. `PropertyValueInc(items, key, value, step, index, direction)` adds `items[i][key] += value + (i * step)`. The `step` parameter adds an incremental offset per item (multiplied by iteration index, not cumulative). The `index` and `direction` parameters control the iteration start point and order (1 = forward, -1 = backward). - -Placement actions like `PlaceOnCircle`, `PlaceOnLine`, `RandomRectangle` accept Phaser geometry objects (`Phaser.Geom.Circle`, `Phaser.Geom.Line`), NOT Game Object shapes. If using a `Phaser.GameObjects.Circle`, pass its `.geom` property instead. - -Action categories: property setters (`SetX`, `SetXY`, `SetAlpha`, `SetScale`, `SetTint`, `SetVisible`, generic `PropertyValueSet`); property incrementers (`IncX`, `IncXY`, `Angle`, `Rotate`, `ScaleXY`, generic `PropertyValueInc`); placement on geometry (`PlaceOnCircle/Ellipse/Line/Rectangle/Triangle`, `RandomCircle/Ellipse/Line/Rectangle/Triangle`); layout and alignment (`GridAlign`, `AlignTo`, `FitToRegion`); distribution (`Spread`, `SmoothStep`, `SmootherStep`); rotation and movement (`RotateAround`, `RotateAroundDistance`, `ShiftPosition` for snake-like trailing, `WrapInRectangle`); queries and iteration (`GetFirst`, `GetLast`, `Call`, `Shuffle`, `ToggleVisible`, `PlayAnimation`); and v4 filter effects (`AddEffectBloom`, `AddEffectShine`, `AddMaskShape`, which return arrays of created effects rather than the input array). - -`Phaser.Utils.Array` offers array manipulation: `Add`, `AddAt`, `Remove`, `RemoveAt`, `RemoveBetween`, `RemoveRandomElement`, `Replace`, `Swap`, `BringToTop`, `SendToBack`, `MoveUp`, `MoveDown`, `MoveAbove`, `MoveBelow`, `MoveTo`, `RotateLeft`, `RotateRight`, `Each`, `EachInRange`, `GetAll`, `GetFirst`, `GetRandom`, `CountAllMatching`, `SetAll`, `Shuffle`, `StableSort`, `SortByDigits`, `QuickSelect`, `FindClosestInSorted`, `Flatten`, `NumberArray`, `NumberArrayStep`, `Range`, `SafeRange`, `SpliceOne`. There is also a `Phaser.Utils.Array.Matrix` sub-namespace for 2D matrix operations. - -`Phaser.Utils.Objects` offers object helpers: `Clone` (shallow), `DeepCopy` (recursive), `Extend` (jQuery-style), `Merge`/`MergeRight`, `Pick`, `GetValue` (dot-path lookup with fallback), `GetFastValue` (top-level-only, faster), `GetAdvancedValue` (resolves random/callback config values), `GetMinMaxValue` (clamped), `SetValue` (dot-path set), `HasValue`, `HasAll`, `HasAny`, `IsPlainObject`. - -`Phaser.Utils.String` offers: `Format` (replace `%1`, `%2` markers), `Pad`, `RemoveAt`, `Reverse`, `UppercaseFirst`, `UUID` (RFC4122 v4). -</core_concepts> - -<common_patterns> -Using Actions with Groups — always pass the array, not the Group: -```js -const enemies = this.add.group({ key: 'enemy', repeat: 9 }); - -// Position all children in a grid -Phaser.Actions.GridAlign(enemies.getChildren(), { - width: 5, height: 2, - cellWidth: 80, cellHeight: 80, - x: 100, y: 50 -}); - -// Fan out alpha across all enemies -Phaser.Actions.Spread(enemies.getChildren(), 'alpha', 0.3, 1); -``` - -The step parameter for staggered values: -```js -// Place sprites starting at x=100, each one 50px further right -Phaser.Actions.SetX(sprites, 100, 50); -// Result: items[0].x=100, items[1].x=150, items[2].x=200, ... - -// Increment rotation with increasing step -Phaser.Actions.Rotate(sprites, 0.1, 0.05); -// Result: items[0].rotation += 0.1, items[1].rotation += 0.15, ... -``` - -Scatter then constrain: -```js -const bounds = new Phaser.Geom.Rectangle(0, 0, 800, 600); -Phaser.Actions.RandomRectangle(sprites, bounds); // random scatter -Phaser.Actions.WrapInRectangle(sprites, bounds); // keep wrapped in bounds during update -``` - -`GetValue` for config parsing: -```js -// Deep property access with fallback -const width = Phaser.Utils.Objects.GetValue(config, 'render.screen.width', 800); - -// Fast top-level lookup (no dot-path, better performance) -const speed = Phaser.Utils.Objects.GetFastValue(config, 'speed', 100); -``` - -`NumberArray` for asset keys: -```js -const keys = Phaser.Utils.Array.NumberArray(1, 10, 'frame_', '.png'); -// Result: ['frame_1.png', 'frame_2.png', ..., 'frame_10.png'] -``` -</common_patterns> - -<gotchas> -- Actions operate on plain arrays, not Groups directly. Always call `group.getChildren()` to get the array. -- PlaceOn/Random geometry actions need `Phaser.Geom` objects, not Game Object shapes. For a `Phaser.GameObjects.Circle`, pass `circle.geom`. -- `SetXY` defaults `y` to `x` if `y` is `undefined` or `null`. Pass `0` explicitly if you want y=0 and x=something else. -- `Spread` with a single item places it at the midpoint `(min + max) / 2`, not at `min`. -- `step` is multiplied by iteration index, not added cumulatively. Item 0 gets `value + 0*step`, item 1 gets `value + 1*step`, etc. -- `StableSort` uses native `Array.sort` when the engine supports stable sorting, falling back to merge sort only when needed. -- `Shuffle` (both Actions and Utils.Array) modifies the array in-place and returns it. -- `GetValue` vs `GetFastValue`: use `GetFastValue` when the key is always top-level (no dots) — it skips dot-path parsing and is faster in hot loops. -- `AddEffectBloom` / `AddEffectShine` / `AddMaskShape` are v4-only filter-based effects. They return arrays of created effects instead of the input. -</gotchas> -</actions_and_utilities> diff --git a/skills/base44-platform-games/references/phaser/animations.md b/skills/base44-platform-games/references/phaser/animations.md deleted file mode 100644 index 5b9855c..0000000 --- a/skills/base44-platform-games/references/phaser/animations.md +++ /dev/null @@ -1,249 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: animations - -<animations> -Use this when creating or controlling sprite animations in Phaser 4: spritesheets, atlases, the AnimationManager and AnimationState, play/stop/chain, frame callbacks, and animation events. - -<quick_start> -Load a spritesheet in `preload`, define a global animation in `create` via `this.anims.create`, then play it on a sprite. - -```js -// In preload -- load a spritesheet -this.load.spritesheet('explosion', 'explosion.png', { - frameWidth: 64, - frameHeight: 64 -}); - -// In create -- define a global animation -this.anims.create({ - key: 'explode', - frames: this.anims.generateFrameNumbers('explosion', { start: 0, end: 11 }), - frameRate: 24, - repeat: 0 -}); - -// Play it on a sprite -const sprite = this.add.sprite(400, 300, 'explosion'); -sprite.play('explode'); -``` -</quick_start> - -<core_concepts> -Phaser has two distinct animation objects. The **AnimationManager** (`this.anims` in a Scene, or `this.game.anims`) is a global singleton owned by the Game — it creates and stores animation definitions, available across every Scene. The **AnimationState** (`sprite.anims`) is a per-sprite instance that controls playback on one Game Object. An `Animation` is a sequence of `AnimationFrame` objects plus timing data, created via `this.anims.create(config)` (global) or `sprite.anims.create(config)` (local to one sprite). - -When `sprite.anims.play(key)` is called, it first checks for a local animation with that key, then falls back to the global AnimationManager — local takes priority. Use local for sprite-specific animations; use global when shared across sprites. - -```js -// Global animation -- available to all sprites -this.anims.create({ key: 'walk', frames: 'player_walk', frameRate: 12, repeat: -1 }); - -// Local animation -- only on this sprite -sprite.anims.create({ key: 'walk', frames: 'npc_walk', frameRate: 10, repeat: -1 }); - -// This plays the LOCAL version because local takes priority -sprite.play('walk'); -``` - -Duration vs frameRate priority: if both are null it defaults to 24 fps; if only `duration` is set, frameRate is calculated as `totalFrames / (duration / 1000)`; if `frameRate` is set (even alongside duration), frameRate wins and duration is derived. Sprite shorthand methods `sprite.play()`, `sprite.playReverse()`, `sprite.chain()`, `sprite.stop()` wrap their `sprite.anims.*` equivalents. -</core_concepts> - -<common_patterns> -Spritesheet animation uses `generateFrameNumbers` (numeric frame indices). Config keys: `start` (default 0), `end` (default -1 = last frame), `first` (single frame to prepend), `frames` (explicit array, overrides start/end). -```js -this.load.spritesheet('dude', 'dude.png', { frameWidth: 32, frameHeight: 48 }); - -// All frames -this.anims.create({ - key: 'run', - frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 7 }), - frameRate: 10, - repeat: -1 -}); - -// Custom frame sequence -this.anims.create({ - key: 'idle', - frames: this.anims.generateFrameNumbers('dude', { frames: [0, 1, 2, 1] }), - frameRate: 6, - repeat: -1 -}); -``` - -Atlas animation uses `generateFrameNames` (string frame names). Config keys: `prefix`, `suffix`, `start`, `end`, `zeroPad` (left-pad numbers to length with zeros), `frames` (explicit, overrides range). Calling `generateFrameNames(key)` with no config returns all frames. -```js -this.load.atlas('gems', 'gems.png', 'gems.json'); - -this.anims.create({ - key: 'ruby_sparkle', - frames: this.anims.generateFrameNames('gems', { - prefix: 'ruby_', - start: 1, - end: 6, - zeroPad: 4 // produces ruby_0001 through ruby_0006 - }), - frameRate: 12, - repeat: -1 -}); -``` - -Pass a texture key string as `frames` to use all frames from that texture, sorted numerically by default (`sortFrames: false` disables sorting): -```js -this.anims.create({ key: 'walk', frames: 'player_walk', frameRate: 12, repeat: -1 }); -``` - -Yoyo and repeat — when `yoyo` is true the animation plays forward then reverses, and the full cycle counts as one play. `repeat: -1` means forever. -```js -this.anims.create({ - key: 'pulse', - frames: this.anims.generateFrameNumbers('orb', { start: 0, end: 5 }), - frameRate: 10, - yoyo: true, - repeat: -1, - repeatDelay: 500 // ms pause between each repeat cycle -}); -``` - -Chaining is per-sprite — chained animations start after `animationcomplete` or `animationstop`. An animation with `repeat: -1` never completes, so call `stop()` to trigger the chain. -```js -sprite.play('attack'); -sprite.chain('idle'); // play idle after attack completes -sprite.chain(['fall', 'land', 'idle']); // chain multiple -sprite.anims.chain(); // clear the chain queue -``` - -Reverse playback: `sprite.playReverse('walk')` sets `forward = false`/`inReverse = true`; `sprite.anims.reverse()` toggles direction mid-playback. - -Play variants: -```js -sprite.play('walk', true); // ignoreIfPlaying = true -sprite.anims.playAfterDelay('walk', 1000); // play after 1s delay -sprite.anims.playAfterRepeat('walk', 2); // play after current anim repeats 2x -``` - -Animation mixing adds a transition delay between two specific animations, set globally on the manager. Mixes apply only with `sprite.play()`, not `playAfterDelay`/`playAfterRepeat`. -```js -this.anims.addMix('idle', 'walk', 200); -this.anims.addMix('walk', 'idle', 300); - -sprite.play('idle'); -sprite.play('walk'); // 200ms mix delay applied automatically - -this.anims.removeMix('idle', 'walk'); // remove specific pair -this.anims.removeMix('idle'); // remove all mixes for 'idle' -``` - -Pause, resume, and stop. All stop methods fire `animationstop` (not `animationcomplete`), and chained animations trigger after stop. -```js -sprite.anims.pause(); // pause per-sprite -sprite.anims.resume(); // resume per-sprite -this.anims.pauseAll(); // global pause -this.anims.resumeAll(); // global resume - -sprite.anims.stop(); // stop immediately -sprite.anims.stopAfterDelay(2000); // stop after 2 seconds -sprite.anims.stopAfterRepeat(1); // stop after 1 more repeat -sprite.anims.stopOnFrame(frame); // stop when a specific frame is reached -``` - -Animation events all share the signature `(animation, frame, gameObject, frameKey)`. Available: `animationstart`, `animationcomplete`, `animationcomplete-{key}`, `animationupdate`, `animationstop`, `animationrepeat`, `animationrestart`. Manager-level events on `this.anims`: `addanimation`, `removeanimation`, `pauseall`, `resumeall`. -```js -sprite.on('animationcomplete', (anim, frame, gameObject, frameKey) => { - console.log('completed:', anim.key); -}); - -// Key-specific complete -- only fires for the named animation -sprite.on('animationcomplete-explode', (anim, frame, gameObject, frameKey) => { - gameObject.destroy(); -}); - -// Frame-level callback via animationupdate -sprite.on('animationupdate', (anim, frame, gameObject, frameKey) => { - if (anim.key === 'attack' && frame.index === 4) { - this.checkHit(gameObject); - } -}); -``` - -Per-frame duration (ms) is added on top of the base msPerFrame: -```js -this.anims.create({ - key: 'combo', - frames: [ - { key: 'fighter', frame: 'punch1', duration: 50 }, - { key: 'fighter', frame: 'kick', duration: 200 }, // hold longer - { key: 'fighter', frame: 'recover', duration: 100 } - ], - frameRate: 24 -}); -``` - -Visibility, random start, and timeScale: -```js -this.anims.create({ - key: 'appear', frames: 'sparkle', frameRate: 12, - showOnStart: true, // sprite.visible = true when anim starts (after delay) - hideOnComplete: true, // sprite.visible = false when anim completes - showBeforeDelay: true // show first frame immediately even during delay -}); - -this.anims.create({ - key: 'ambient', frames: 'fire', frameRate: 10, repeat: -1, - randomFrame: true // each sprite begins on a different frame -}); - -sprite.anims.timeScale = 2; // 2x speed -sprite.play({ key: 'walk', timeScale: 0.5 }); // half speed via config -this.anims.globalTimeScale = 0.5; // affects ALL animations -``` - -Staggered playback across a group — each sprite starts after the previous (pass `staggerFirst: false` to skip the delay on the first): -```js -const enemies = this.add.group({ key: 'enemy', repeat: 9 }); -this.anims.staggerPlay('walk', enemies.getChildren(), 100); -``` - -JSON export/import and existence checks: -```js -const data = this.anims.toJSON(); -this.anims.fromJSON(data); // import -this.anims.fromJSON(data, true); // pass true to clear existing first - -if (!this.anims.exists('walk')) { - this.anims.create({ key: 'walk', frames: 'player_walk', frameRate: 12, repeat: -1 }); -} -``` - -Modifying animation frames at runtime: -```js -const anim = this.anims.get('walk'); -anim.addFrame(this.anims.generateFrameNumbers('player', { start: 8, end: 10 })); // append -anim.addFrameAt(this.anims.generateFrameNumbers('player', { frames: [5] }), 2); // insert at index -anim.removeFrame(anim.frames[3]); // remove a frame object -anim.removeFrameAt(0); // remove frame at index -``` - -Aseprite support creates animations from Aseprite tags: -```js -this.load.aseprite('paladin', 'paladin.png', 'paladin.json'); -// In create: -this.anims.createFromAseprite('paladin'); // all tags -this.anims.createFromAseprite('paladin', ['walk']); // specific tags only -sprite.play('walk'); // play by tag name -``` -</common_patterns> - -<gotchas> -1. Animations are global by default. `this.anims.create()` registers across all Scenes. Do not recreate in every Scene — it logs a warning and returns the existing one. -2. `repeat: -1` never fires `animationcomplete`. Use `stop()` to end infinite animations; listen for `animationstop` instead. -3. `frameRate` beats `duration`. If both are set, frameRate wins. Set only `duration` (leave frameRate null) to control total length. -4. Per-frame `duration` is additive — added on top of base msPerFrame, not a replacement. -5. `play()` stops the current animation (fires `animationstop`). Use `play(key, true)` to skip if already playing. -6. Mix delays only work with `play()`. `playAfterDelay`/`playAfterRepeat` bypass mixes. -7. Local animations override global. Same key on a sprite's local map takes priority. -8. Sprite shorthand methods (`sprite.play()`, `sprite.playReverse()`, `sprite.chain()`, `sprite.stop()`) wrap `sprite.anims.*`. -9. Chained anims fire after stop too. Clear the queue with `sprite.anims.chain()` before stopping if unwanted. -10. `generateFrameNumbers` end=-1 means last frame. The `__BASE` frame is excluded automatically. -</gotchas> -</animations> diff --git a/skills/base44-platform-games/references/phaser/audio_and_sound.md b/skills/base44-platform-games/references/phaser/audio_and_sound.md deleted file mode 100644 index 6975f5a..0000000 --- a/skills/base44-platform-games/references/phaser/audio_and_sound.md +++ /dev/null @@ -1,229 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: audio and sound - -<audio_and_sound> -Use this when adding audio or sound to a Phaser 4 game: loading audio, playing sounds and music, volume and panning, looping, markers, audio sprites, spatial audio, and the SoundManager. - -<quick_start> -`this.sound` is the SoundManager, available in any Scene. Use `play()` for fire-and-forget sounds or `add()` to retain a reference for ongoing control. Assets loaded via `this.load.audio()` in `preload()` are ready by the time `create()` runs; provide an array of URLs for cross-browser format fallback. - -```js -class GameScene extends Phaser.Scene { - preload() { - this.load.audio('bgm', 'assets/music.mp3'); - this.load.audio('coin', ['assets/coin.ogg', 'assets/coin.mp3']); - } - - create() { - // Fire-and-forget (auto-destroys when complete) - this.sound.play('coin'); - - // Retained reference for ongoing control - this.music = this.sound.add('bgm', { loop: true, volume: 0.5 }); - this.music.play(); - } -} -``` -</quick_start> - -<core_concepts> -Phaser auto-selects the best backend via `SoundManagerCreator.create()`. If `config.audio.noAudio` is true or the device supports neither backend, a **NoAudioSoundManager** is created (all calls are no-ops). If the device supports Web Audio and `disableWebAudio` is not true, a **WebAudioSoundManager** is created (preferred). Otherwise an **HTML5AudioSoundManager** is the fallback. Force a backend via game config: `audio: { disableWebAudio: true }` or `audio: { noAudio: true }`. Pass `audio: { context: existingAudioContext }` to reuse a WebAudio context in SPAs. - -WebAudio advantages: precise timing, gapless looping, stereo panning (`StereoPannerNode`), spatial audio (`PannerNode`), per-sound gain nodes, runtime `decodeAudio()`. HTML5 Audio limitations: no spatial audio, no real stereo panning (pan fires events but no audible effect), less precise looping, and it requires an `instances` count at load time for simultaneous playback. - -The SoundManager (`this.sound`) is a single shared instance across the entire game. It handles adding/playing/removing sound instances, global volume/mute/rate/detune, automatic pause/resume on tab blur (`pauseOnBlur`, default `true`), mobile audio unlocking, and the spatial audio listener position (WebAudio only). - -Sound instances are created via `this.sound.add(key, config)`. A sound must exist in the audio cache (loaded via the Loader) before it can be added. Each instance has its own playback state and `volume`, `rate`, `detune`, `loop`, `pan`, and `seek` properties. State flags: `isPlaying`, `isPaused`. - -```js -const sfx = this.sound.add('explosion', { volume: 0.8 }); -sfx.play(); // returns boolean -sfx.pause(); // only works if isPlaying -sfx.resume(); // only works if isPaused -sfx.stop(); // resets to stopped state -sfx.destroy(); // marks for removal from manager -``` -</core_concepts> - -<common_patterns> -Playing sounds — fire-and-forget (`this.sound.play(key, config?)` adds, plays, and auto-destroys on completion) or retained reference (`this.sound.add(key, config?)` then `play()`): -```js -this.sound.play('explosion'); -this.sound.play('powerup', { volume: 0.5, rate: 1.2 }); - -const laser = this.sound.add('laser'); -laser.play(); -// Later: laser.stop(), laser.volume = 0.3, etc. -``` - -Volume, rate, and detune — settable per-sound or globally on the manager; values combine (rate/detune multiply via `calculateRate()`). The effective playback rate is `sound.rate * manager.rate * detuneRate` where `detuneRate = Math.pow(1.0005777895065548, sound.detune + manager.detune)`. All `set*` methods return `this` for chaining. -```js -// Per-sound -sound.volume = 0.5; // 0 to 1 -sound.setVolume(0.5); // chainable -sound.rate = 1.5; // 0.5 = half speed, 2.0 = double speed -sound.detune = 200; // cents, -1200 to 1200 - -// Global (affects all sounds) -this.sound.volume = 0.8; -this.sound.rate = 1.0; -this.sound.detune = 0; -``` - -Looping — `LOOPED` fires each time the sound loops; `LOOP` fires when the loop property changes: -```js -const bgm = this.sound.add('music', { loop: true }); -bgm.play(); -bgm.loop = false; -bgm.setLoop(false); // chainable -``` - -Seeking — setting seek on a stopped sound has no effect: -```js -sound.seek = 5.0; // jump to 5 seconds in -sound.setSeek(5.0); // chainable -console.log(sound.seek); // current playback position in seconds -``` - -Stereo panning — uses `StereoPannerNode` on WebAudio; on HTML5 Audio the property fires events but has no audible effect: -```js -sound.pan = -1; // full left -sound.pan = 0; // center -sound.pan = 1; // full right -sound.setPan(0.5); // chainable -``` - -Audio sprites combine multiple sounds into one file with a JSON config (from the `audiosprite` tool); the JSON must be loaded separately. Its `spritemap` entries auto-convert to markers. You can also add markers manually to any sound via `addMarker(marker)`, `updateMarker(marker)`, `removeMarker(name)`. -```js -// In preload -this.load.audioSprite('sfx', 'assets/sfx.json', ['assets/sfx.ogg', 'assets/sfx.mp3']); - -// In create -this.sound.playAudioSprite('sfx', 'explosion'); -this.sound.playAudioSprite('sfx', 'coin', { volume: 0.5 }); - -// Or add for retained control -const sprite = this.sound.addAudioSprite('sfx'); -sprite.play('explosion'); - -// Manual markers on any sound -const sound = this.sound.add('longtrack'); -sound.addMarker({ name: 'intro', start: 0, duration: 5 }); -sound.addMarker({ name: 'loop', start: 5, duration: 20, config: { loop: true } }); -sound.play('intro'); -sound.play('loop'); // later -``` - -Background music — `pauseOnBlur` (default `true`) auto-pauses all sounds when the tab loses focus: -```js -this.bgm = this.sound.add('theme', { loop: true, volume: 0.4 }); -this.bgm.play(); -// Stop on scene shutdown: this.bgm.stop(); -``` - -Spatial audio (WebAudio only) uses a `PannerNode` to position sounds relative to a listener. Set `sound.x`/`sound.y` directly to reposition at any time; if `follow` is set, the position updates each frame. `setListenerPosition()` with no arguments defaults to the canvas center. -```js -this.sound.setListenerPosition(400, 300); -// Or: this.sound.listenerPosition.set(x, y); - -const enemy = this.sound.add('roar', { - source: { - x: 800, y: 300, - refDistance: 50, maxDistance: 2000, rolloffFactor: 1, - distanceModel: 'inverse', panningModel: 'equalpower', - follow: enemySprite // auto-track a Game Object's x/y - } -}); -enemy.play(); -``` - -Muting (per-sound or global): -```js -sound.mute = true; sound.setMute(true); -this.sound.mute = true; this.sound.setMute(true); -``` - -Querying sounds: -```js -this.sound.get('coin'); // first sound with key, or null -this.sound.getAll('coin'); // all sounds with key -this.sound.getAll(); // every sound in the manager -this.sound.getAllPlaying(); // all currently playing sounds -this.sound.isPlaying('coin'); // true if any 'coin' sound is playing -this.sound.isPlaying(); // true if any sound is playing -``` - -Removing and stopping: -```js -this.sound.stopAll(); // stop all, fires STOP_ALL -this.sound.stopByKey('coin'); // stop all with key, returns count -this.sound.pauseAll(); // fires PAUSE_ALL -this.sound.resumeAll(); // fires RESUME_ALL - -this.sound.remove(soundInstance); // destroy + remove specific sound -this.sound.removeByKey('coin'); // destroy + remove all with key, returns count -this.sound.removeAll(); // destroy + remove everything -``` - -Decoding audio at runtime (WebAudio only): -```js -this.sound.decodeAudio('key', base64StringOrArrayBuffer); -// Batch: this.sound.decodeAudio([{ key: 'sfx1', data: buf1 }, { key: 'sfx2', data: buf2 }]); -this.sound.on('decoded', (key) => { /* one done */ }); -this.sound.on('decodedall', () => { /* all done */ }); -``` -</common_patterns> - -<events> -Sound instance events (emitted on a Sound object): `PLAY` `'play'` `(sound)`, `PAUSE` `'pause'` `(sound)`, `RESUME` `'resume'` `(sound)`, `STOP` `'stop'` `(sound)`, `COMPLETE` `'complete'` `(sound)` (non-looping finish), `LOOPED` `'looped'` `(sound)`, `LOOP` `'loop'` `(sound, value)`, `MUTE` `'mute'` `(sound, value)`, `VOLUME` `'volume'`, `RATE` `'rate'`, `DETUNE` `'detune'`, `SEEK` `'seek'`, `PAN` `'pan'`, `DESTROY` `'destroy'` `(sound)`. - -SoundManager events (emitted on `this.sound`): `PAUSE_ALL` `'pauseall'`, `RESUME_ALL` `'resumeall'`, `STOP_ALL` `'stopall'` (each `(manager)`); `GLOBAL_MUTE` `'globalmute'`, `GLOBAL_VOLUME` `'globalvolume'`, `GLOBAL_RATE` `'globalrate'`, `GLOBAL_DETUNE` `'globaldetune'` (each `(manager, value)`); `UNLOCKED` `'unlocked'` `(manager)` (audio system unlocked after user interaction); `DECODED` `'decoded'` `(key)` and `DECODED_ALL` `'decodedall'` `()` (WebAudio decoding). - -```js -const sfx = this.sound.add('bang'); -sfx.on('complete', (sound) => { - console.log(sound.key, 'finished'); -}); -sfx.play(); -``` -</events> - -<gotchas> -Browser autoplay policy — browsers block audio until user interaction, and Phaser handles unlocking automatically. On WebAudio the `AudioContext` starts suspended and Phaser listens for touch/mouse/key events on `document.body` to call `context.resume()`; the `locked` property is true until unlocked and `UNLOCKED` fires once resolved. On HTML5 Audio, locked tags queue actions until the first touch. You do not need to handle unlocking manually, but you can wait for readiness: -```js -if (this.sound.locked) { - this.sound.once('unlocked', () => { this.sound.play('bgm'); }); -} else { - this.sound.play('bgm'); -} -``` - -Audio format support — no single format works everywhere. Provide multiple formats: `this.load.audio('bgm', ['assets/bgm.ogg', 'assets/bgm.mp3'])`. MP3 has broadest support; OGG Vorbis lacks Safari support; AAC/M4A works on Safari/iOS; WebM/Opus has excellent quality but limited older-browser support. - -HTML5 Audio simultaneous playback uses a pool of `<audio>` tags. Specify `instances` when loading for simultaneous playback: `this.load.audio('shot', 'assets/shot.mp3', { instances: 4 })` (default 1). If all tags are in use and `manager.override` is true (default), the sound with the most progress is hijacked. WebAudio has no such limit. - -iOS/Safari specifics — `StereoPannerNode` is unsupported, so `pan` has no audible effect (events still fire). iOS 17/18+ can interrupt audio on background; Phaser handles this via `context.suspend()`/`context.resume()` on the `VISIBLE` game event. `setListenerPosition()` and spatial audio are WebAudio-only. - -WebAudio context reuse — for SPAs that recreate the game without a full page reload, pass `audio: { context: existingAudioContext }` in config. You can also swap contexts at runtime via `this.sound.setAudioContext(newContext)` (WebAudio only). - -SoundManager is shared (global) — one per game, not per scene. Sounds are not automatically cleaned up on scene shutdown; you must stop/remove them yourself. Looping sounds continue across scene changes unless explicitly stopped. - -Fire-and-forget vs persistent — `this.sound.play(key)` creates a sound that auto-destroys on completion and cannot be controlled afterward. Use `this.sound.add(key)` when you need to pause, stop, adjust, or listen for events. - -Spatial audio is WebAudio only — the `source` config is silently ignored on HTML5 Audio. Do not rely on spatial positioning for gameplay-critical cues if HTML5 fallback must be supported. - -Web Audio Analyser (WebAudio only) — access the underlying `AudioContext` to create an `AnalyserNode` for frequency/waveform visualization. Check `this.sound.context` exists first. -```js -const analyser = this.sound.context.createAnalyser(); -analyser.fftSize = 256; -this.sound.masterVolumeNode.connect(analyser); -analyser.connect(this.sound.context.destination); - -const dataArray = new Uint8Array(analyser.frequencyBinCount); -// In update loop: -analyser.getByteFrequencyData(dataArray); // drive visual effects from dataArray -``` -</gotchas> -</audio_and_sound> diff --git a/skills/base44-platform-games/references/phaser/cameras.md b/skills/base44-platform-games/references/phaser/cameras.md deleted file mode 100644 index 10a1dd2..0000000 --- a/skills/base44-platform-games/references/phaser/cameras.md +++ /dev/null @@ -1,250 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: cameras - -<cameras> -Use this when working with cameras in Phaser 4: the CameraManager and main camera, viewport vs scroll, zoom, bounds, following sprites, camera effects (fade, flash, shake, pan, zoomTo, rotateTo), ignore lists, filters, and keyboard controls. - -<quick_start> -Every Scene has a default camera at `this.cameras.main`. Access it in `create()` to scroll, zoom, follow, bound, and apply effects. - -```js -// In a Scene's create() method: - -// Access the default camera (created automatically) -const cam = this.cameras.main; - -// Scroll the camera to look at a different part of the world -cam.setScroll(200, 100); - -// Center camera on a world coordinate -cam.centerOn(400, 300); - -// Zoom in (2x) -- values < 1 zoom out, > 1 zoom in -cam.setZoom(2); - -// Follow a sprite with smooth lerp -cam.startFollow(player, false, 0.1, 0.1); - -// Constrain the camera to the world bounds -cam.setBounds(0, 0, 2048, 2048); - -// Fade in from black over 1 second -cam.fadeIn(1000); - -// Add a filter to the camera (v4 feature) -cam.filters.external.addBlur(1, 2); -``` -</quick_start> - -<core_concepts> -Every Scene has a `CameraManager` at `this.cameras` (not `this.camera`), registered as a plugin under the key `'CameraManager'`. Key references: `this.cameras.cameras` (array of Camera objects in render order), `this.cameras.main` (the "main" camera, `cameras[0]` by default), `this.cameras.default` (an un-transformed utility camera, not in the cameras array). - -Key CameraManager methods: `add(x?, y?, width?, height?, makeMain?, name?)` creates a Camera (defaults to full game size at 0,0); `addExisting(camera, makeMain?)` adds a pre-built Camera; `remove(camera, runDestroy?)` removes and optionally destroys (if main is removed, resets to `cameras[0]`); `getCamera(name)` finds by name; `getTotal(isVisible?)` counts cameras; `fromJSON(config)` creates cameras from config; `resetAll()` destroys all and creates one fresh default; `resize(width, height)` resizes all cameras. The manager supports up to 32 cameras that can use `ignore()` for object exclusion (IDs are bitmasks); cameras beyond 32 get ID 0 and cannot exclude objects. - -The `main` property is a convenience reference, set automatically when the scene boots (first camera created becomes main), when you pass `makeMain: true` to `add`/`addExisting`, or when the current main is removed (falls back to `cameras[0]`). - -A Camera has two independent coordinate concepts. The **viewport** is the physical rectangle on the canvas where it renders, controlled by `setPosition(x, y)`, `setSize(w, h)`, or `setViewport(x, y, w, h)` (defaults to filling the canvas). **Scroll** is where the camera is looking in the world, controlled by `scrollX`/`scrollY` or `setScroll(x, y)` — scrolling does not affect the viewport. `worldView` is a read-only `Rectangle` updated each frame reflecting what world area the camera can currently see (accounting for scroll, zoom, and bounds), useful for culling or intersection checks. - -```js -// Viewport: a 320x200 mini-map in the top-right corner -const miniCam = this.cameras.add(480, 0, 320, 200); -// Scroll: make the mini-map look at a different world area -miniCam.setScroll(1000, 500); -// Zoom: the mini-cam shows more of the world -miniCam.setZoom(0.25); -``` -</core_concepts> - -<common_patterns> -Scrolling: -```js -cam.scrollX = 100; cam.scrollY = 200; // direct property access -cam.setScroll(100, 200); // chainable setter -cam.centerOn(500, 400); // center on a world coordinate -const point = cam.getScroll(500, 400); // scroll values to center on a point (Vector2), without moving -``` - -Following a sprite — `startFollow` accepts any object with `x`/`y` (not just Game Objects). Lerp of `1` snaps instantly, `0.1` gives smooth tracking, `0` on an axis disables tracking on that axis. With a deadzone set, the camera does not scroll while the target remains inside the deadzone rectangle (re-centered on the camera midpoint each frame). -```js -cam.startFollow(player); // instant follow (lerp = 1, default) -cam.startFollow(player, false, 0.1, 0.1); // smooth follow -// startFollow(target, roundPixels?, lerpX?, lerpY?, offsetX?, offsetY?) -cam.startFollow(player, true, 0.05, 0.05, 0, -50); - -cam.setLerp(0.08, 0.08); // change lerp after starting -cam.setFollowOffset(0, -50); // change offset after starting -cam.setDeadzone(200, 150); // camera only scrolls when target leaves this rect -cam.stopFollow(); -``` - -Zoom — never set zoom to 0; the minimum is clamped to 0.001: -```js -cam.setZoom(2); // 2x zoom in -cam.setZoom(0.5); // zoom out (see twice as much) -cam.setZoom(2, 1); // independent horizontal/vertical zoom -cam.zoom; // shortcut -- reads zoomX (assumes uniform) -cam.zoomX; cam.zoomY; -``` - -Bounds — bounds only restrict scrolling; they do not prevent placing Game Objects outside, nor affect the viewport position: -```js -cam.setBounds(0, 0, worldWidth, worldHeight); -cam.setBounds(0, 0, 2048, 2048, true); // center on new bounds immediately -cam.useBounds = false; // temporarily disable without removing -cam.removeBounds(); // remove entirely -const rect = cam.getBounds(); // returns a new Rectangle copy -``` - -Multiple cameras — a dual-camera layering pattern (minimaps, in-canvas effect layers; screen-space HUD belongs in the React overlay): one camera ignores world objects and the other ignores layer objects: -```js -const main = this.cameras.main; - -const minimap = this.cameras.add(600, 0, 200, 150).setZoom(0.2).setName('minimap'); -minimap.setScroll(0, 0); -minimap.setBackgroundColor('rgba(0,0,0,0.5)'); - -const hudCam = this.cameras.add(0, 0, 800, 600).setName('hud'); -hudCam.setScroll(0, 0); - -main.ignore(hudGroup); // main ignores HUD objects -hudCam.ignore(worldGroup); // HUD camera ignores world objects - -const found = this.cameras.getCamera('minimap'); -this.cameras.remove(minimap); -``` - -Camera effects — all are on the `Camera` class (not `BaseCamera`), each returns `this` for chaining, and an already-running effect will not restart unless you pass `force: true`. - -Fade — `fadeOut`/`fade` goes transparent-to-color; `fadeIn`/`fadeFrom` goes color-to-transparent: -```js -cam.fadeOut(1000); // to black over 1s -cam.fadeOut(1000, 255, 0, 0); // to red -cam.fadeIn(500); // from black over 500ms -cam.fade(1000, 0, 0, 0, true); // lower-level, force start even if running -cam.fadeFrom(1000, 0, 0, 0, true); -cam.fadeOut(1000, 0, 0, 0, (cam, progress) => { /* progress 0..1 */ }); -``` - -Flash — `flash(duration?, r?, g?, b?, force?, callback?, context?)`: -```js -cam.flash(250); // white flash, default 250ms -cam.flash(500, 255, 0, 0); // red flash -cam.flash(300, 255, 255, 255, true, (cam, progress) => {}); -``` - -Shake — `shake(duration?, intensity?, force?, callback?, context?)`. The intensity is a small float; the default 0.05 shifts up to 5% of the viewport size: -```js -cam.shake(100); // 100ms at intensity 0.05 -cam.shake(500, 0.02); // stronger, longer -cam.shake(300, new Phaser.Math.Vector2(0.1, 0.02)); // independent x/y intensity -``` - -Pan — scrolls the camera so its viewport center finishes at the given world coordinate; `pan(x, y, duration?, ease?, force?, callback?, context?)`: -```js -cam.pan(400, 300, 2000); -cam.pan(400, 300, 2000, 'Power2'); -cam.pan(800, 600, 1500, 'Sine.easeInOut', false, (cam, progress, x, y) => {}); -``` - -ZoomTo — `zoomTo(zoom, duration?, ease?, force?, callback?, context?)`: -```js -cam.zoomTo(2, 1000); -cam.zoomTo(0.5, 2000, 'Cubic.easeOut'); -``` - -RotateTo — angle is in **radians**; set `shortestPath` to take the shortest direction; `rotateTo(angle, shortestPath?, duration?, ease?, force?, callback?, context?)`: -```js -cam.rotateTo(Phaser.Math.DegToRad(45), false, 1000); -cam.rotateTo(Math.PI, true, 1500, 'Quad.easeInOut'); -``` - -Reset all running effects (rotate, pan, shake, flash, fade): -```js -cam.resetFX(); -``` - -Keyboard controls — both classes require `update(delta)` in your scene's update method and support `zoomIn`/`zoomOut` keys plus a `zoomSpeed` config value. `FixedKeyControl` moves at a fixed speed per frame (no smoothing); `SmoothedKeyControl` applies acceleration, drag, and max speed. -```js -// FixedKeyControl -const cursors = this.input.keyboard.createCursorKeys(); -this.camControl = new Phaser.Cameras.Controls.FixedKeyControl({ - camera: this.cameras.main, - left: cursors.left, right: cursors.right, up: cursors.up, down: cursors.down, - speed: 0.5 // can also be { x: 0.5, y: 0.3 } -}); -// In update(time, delta): this.camControl.update(delta); - -// SmoothedKeyControl -this.camControl = new Phaser.Cameras.Controls.SmoothedKeyControl({ - camera: this.cameras.main, - left: cursors.left, right: cursors.right, up: cursors.up, down: cursors.down, - zoomIn: this.input.keyboard.addKey('Q'), - zoomOut: this.input.keyboard.addKey('E'), - zoomSpeed: 0.02, acceleration: 0.06, drag: 0.0005, maxSpeed: 1.0 -}); -// In update(time, delta): this.camControl.update(delta); -``` - -Ignore lists — `ignore` updates a Game Object's `cameraFilter` bitmask so the camera skips rendering it. This is the primary mechanism for layered-camera setups such as minimaps. -```js -cam.ignore(uiText); // single object -cam.ignore([scoreText, livesIcon, pauseButton]); // array -cam.ignore(uiGroup); // all children in a Group -cam.ignore(uiLayer); // a Layer and all its children -``` - -Camera deadzone — a rectangular area around the follow target where the camera does not scroll: -```js -cam.setDeadzone(200, 150); // 200x150 centered on the viewport -console.log(cam.deadzone); // Phaser.Geom.Rectangle or null (read-only) -cam.setDeadzone(); // remove -``` - -World point conversion — essential with scrolled/zoomed cameras, since raw pointer coordinates are screen space: -```js -const worldPoint = cam.getWorldPoint(pointer.x, pointer.y); -const output = new Phaser.Math.Vector2(); // reuse to avoid allocation -cam.getWorldPoint(pointer.x, pointer.y, output); -``` - -Render list — `cam.renderList` is an array of Game Objects visible to this camera, rebuilt every frame. - -Force composite — render the camera to an offscreen framebuffer (required for `CaptureFrame` and similar). Filters enable this automatically; use it explicitly when you need it without filters: -```js -cam.setForceComposite(true); -cam.setForceComposite(false); -``` - -Filters on cameras (v4) — `Camera` has a `filters` property with two `FilterList` instances: `cam.filters.internal` (system-applied) and `cam.filters.external` (user-added). Add post-processing the same way as on Game Objects. Filters require WebGL and force framebuffer rendering when active. -```js -cam.filters.external.addBlur(1, 2); -cam.filters.external.addGlow(0xff0000, 4, 0, false, 0.1, 10); -const fx = cam.filters.external.addColorMatrix(); -fx.grayscale(); -cam.filters.external.clear(); // remove all external filters -``` -</common_patterns> - -<events> -Camera events are emitted on the Camera instance itself (it extends `EventEmitter`); listen with `cam.on(event, handler)`. Constants live under `Phaser.Cameras.Scene2D.Events`: - -- `FADE_IN_START` / `FADE_IN_COMPLETE` — `fadeIn`/`fadeFrom` begins / fade-in finishes -- `FADE_OUT_START` / `FADE_OUT_COMPLETE` — `fadeOut`/`fade` begins / fade-out finishes -- `FLASH_START` / `FLASH_COMPLETE` -- `SHAKE_START` / `SHAKE_COMPLETE` -- `PAN_START` / `PAN_COMPLETE` -- `ZOOM_START` / `ZOOM_COMPLETE` -- `ROTATE_START` / `ROTATE_COMPLETE` -- `FOLLOW_UPDATE` — each frame while following; args `(camera, target)` -- `PRE_RENDER` / `POST_RENDER` — before / after the camera renders -- `DESTROY` — camera is destroyed - -```js -cam.on(Phaser.Cameras.Scene2D.Events.FADE_OUT_COMPLETE, () => { - this.scene.start('GameOver'); -}); -``` -</events> -</cameras> diff --git a/skills/base44-platform-games/references/phaser/curves_and_paths.md b/skills/base44-platform-games/references/phaser/curves_and_paths.md deleted file mode 100644 index 0e04259..0000000 --- a/skills/base44-platform-games/references/phaser/curves_and_paths.md +++ /dev/null @@ -1,182 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: curves and paths - -<curves_and_paths> -Use this when working with curves and paths in Phaser 4: creating paths from curves (splines, bezier, lines, ellipses), getting points along them, drawing them with Graphics, and making sprites follow paths automatically with PathFollower. - -<quick_start> -```js -// In a Scene's create() method: - -// 1. Create a Path starting at (50, 300) -const path = this.add.path(50, 300); - -// 2. Add curves to the path -path.lineTo(200, 100); -path.splineTo([ new Phaser.Math.Vector2(300, 400), new Phaser.Math.Vector2(500, 200) ]); -path.lineTo(700, 300); - -// 3. Draw the path using Graphics -const graphics = this.add.graphics(); -graphics.lineStyle(2, 0xffffff, 1); -path.draw(graphics, 64); - -// 4. Create a PathFollower sprite that moves along the path -const follower = this.add.follower(path, 50, 300, 'ship'); -follower.startFollow({ - duration: 5000, - rotateToPath: true, - repeat: -1, - yoyo: true -}); -``` -</quick_start> - -<core_concepts> -A `Phaser.Curves.Path` is a container that combines multiple Curves into one continuous compound curve. Curves in a Path do not need to be connected end-to-end; only their order affects point calculations. Created via `this.add.path(x, y)` where x/y is the starting point. Key properties: `curves` (array of `Phaser.Curves.Curve`), `startPoint` (Vector2), `autoClose` (if true, `getPoints()` appends the first point at the end), `defaultDivisions` (default 12, divisions per curve for `getPoints()`), `name`. - -All curve types extend `Phaser.Curves.Curve`. Every curve supports: `getPoint(t, out)` (point at parameter t 0-1), `getPointAt(u, out)` (point at arc-length position u 0-1, evenly spaced), `getPoints(divisions, stepRate, out)`, `getSpacedPoints(divisions, stepRate, out)` (equidistant by arc length), `getDistancePoints(distance)`, `getLength()`, `getBounds(out, accuracy)`, `getTangent(t, out)` / `getTangentAt(u, out)` (unit tangent vector), `getStartPoint(out)` / `getEndPoint(out)`, `getRandomPoint(out)`, `draw(graphics, pointsTotal)`, and `active` (when false, the parent Path skips this curve). - -A `Phaser.GameObjects.PathFollower` is a Sprite with the `Components.PathFollower` mixin. It uses an internal Tween (a number counter from 0 to 1) to advance along a Path each frame. Created via `this.add.follower(path, x, y, texture, frame)`. The component provides: `path` (the Path being followed), `pathTween` (internal Tween driving movement), `pathOffset` (Vector2 offset added to path coordinates), `pathVector` (current position on path), `pathDelta` (distance traveled since last frame), `rotateToPath` (auto-rotate to face path direction), `pathRotationOffset` (degrees added to auto-rotation). -</core_concepts> - -<common_patterns> -Path has convenience methods that create curves starting from the previous end point: -```js -const path = this.add.path(100, 500); - -path.lineTo(300, 100); // straight line -path.cubicBezierTo(500, 100, 350, 50, 450, 50); // cubic bezier (endX, endY, cp1X, cp1Y, cp2X, cp2Y) -path.quadraticBezierTo(700, 400, 600, 100); // quadratic bezier (endX, endY, cpX, cpY) -path.splineTo([ // spline through points - new Phaser.Math.Vector2(750, 300), - new Phaser.Math.Vector2(600, 500) -]); -path.ellipseTo(50, 80, 0, 270, false, 0); // ellipse arc (xRadius, yRadius, startAngle, endAngle, clockwise, rotation) -path.circleTo(40); // shortcut for ellipseTo with equal radii and 0-360 - -// Jump to a new position without drawing (creates a gap) -path.moveTo(400, 400); -path.lineTo(500, 400); - -// Close the path by connecting end to start -path.closePath(); -``` - -Adding pre-constructed standalone curve objects: -```js -const path = new Phaser.Curves.Path(0, 0); - -const line = new Phaser.Curves.Line(new Phaser.Math.Vector2(0, 0), new Phaser.Math.Vector2(200, 200)); -path.add(line); - -const spline = new Phaser.Curves.Spline([ 200, 200, 300, 100, 400, 300 ]); -path.add(spline); - -const ellipse = new Phaser.Curves.Ellipse(400, 300, 100, 60, 0, 360, false, 0); -path.add(ellipse); -``` - -Getting points along a path: -```js -const points = path.getPoints(); // array of points (uses defaultDivisions per curve) -const detailed = path.getPoints(32); // explicit divisions per curve -const spaced = path.getSpacedPoints(100); // equally spaced points along entire path -const midpoint = path.getPoint(0.5); // single point at normalized position (0-1) -const tangent = path.getTangent(0.5); // tangent vector at a position -const length = path.getLength(); // total path length in pixels -const bounds = path.getBounds(); // bounding rectangle -``` - -Drawing paths with Graphics: -```js -const graphics = this.add.graphics(); - -// Draw entire path -graphics.lineStyle(2, 0x00ff00, 1); -path.draw(graphics, 64); // 64 = points per curve for smoothness - -// Draw individual curves -graphics.lineStyle(1, 0xff0000, 1); -path.curves[0].draw(graphics, 32); - -// Draw debug points -const points = path.getSpacedPoints(50); -points.forEach(p => { - graphics.fillStyle(0xffff00, 1); - graphics.fillCircle(p.x, p.y, 3); -}); -``` - -PathFollower sprite with full config: -```js -const path = this.add.path(100, 200); -path.lineTo(400, 400); -path.lineTo(700, 200); - -const enemy = this.add.follower(path, 100, 200, 'enemy'); - -enemy.startFollow({ - duration: 3000, // ms to traverse path - positionOnPath: true, // snap to path start position - rotateToPath: true, // auto-rotate to face direction - rotationOffset: 90, // offset added to auto-rotation (degrees) - repeat: -1, // -1 = infinite repeat - yoyo: true, // reverse on each repeat - from: 0, // start position on path (0-1) - to: 1, // end position on path (0-1) - startAt: 0, // initial seek position - ease: 'Sine.easeInOut' // any valid Phaser ease -}); - -// Control during playback -enemy.pauseFollow(); -enemy.resumeFollow(); -enemy.stopFollow(); -enemy.isFollowing(); // returns boolean - -// Change path at runtime -enemy.setPath(newPath); -enemy.setPath(newPath, { duration: 2000 }); // auto-starts - -// Set rotation independently -enemy.setRotateToPath(true, 90); // (value, offsetDegrees) -``` - -The follow config also accepts a bare duration number as shorthand — `enemy.startFollow(5000)` is equivalent to `enemy.startFollow({ duration: 5000 })`. The config additionally accepts all standard Tween properties (`ease`, `repeat`, `yoyo`, `delay`, `hold`, `onComplete`, etc.); defaults are `duration: 1000`, `from: 0`, `to: 1`, `positionOnPath: false`, `rotateToPath: false`, `rotationOffset: 0`, `startAt: 0`. -</common_patterns> - -<curve_types> -- **Line** (`Phaser.Curves.Line`): straight segment between two points. Params `(p0, p1)` as Vector2 or `([x0,y0,x1,y1])`. -- **Spline** (`Phaser.Curves.Spline`): Catmull-Rom spline through control points. Params `(points)` as array of Vector2, flat numbers, or nested arrays. -- **CubicBezier** (`Phaser.Curves.CubicBezier`): start, 2 control points, end. Params `(p0, p1, p2, p3)` or `([x0,y0,...x3,y3])`. -- **QuadraticBezier** (`Phaser.Curves.QuadraticBezier`): start, 1 control point, end. Params `(p0, p1, p2)` or `([x0,y0,...x2,y2])`. -- **Ellipse** (`Phaser.Curves.Ellipse`): elliptical arc, angles in degrees, yRadius defaults to xRadius. Params `(x, y, xRadius, yRadius, startAngle, endAngle, clockwise, rotation)` or a config object. - -The Ellipse curve has get/set properties for runtime modification: `x`, `y` (center), `xRadius`, `yRadius`, `startAngle`/`endAngle` (degrees — internally converted to/from radians), `clockwise`, `rotation` (radians), `angle` (rotation in degrees, alternative to `rotation`), and `setWidth(value)` / `setHeight(value)` (set radius to value/2). -</curve_types> - -<gotchas> -1. **`getPoint(t)` vs `getPointAt(u)` on curves.** `getPoint` uses the raw curve parameter t, which does not produce evenly spaced points on most curve types. `getPointAt` maps through arc length for even spacing. On a Path, `getPoint` already accounts for arc length across the whole path. - -2. **Path `moveTo` creates an inactive curve.** The `MoveTo` pseudo-curve has `active: false` and zero length. It only repositions the end point for the next curve — it does not draw anything and is skipped by `getPoints()` and `draw()`. - -3. **PathFollower uses a Tween internally.** The `startFollow` config is passed to `scene.tweens.addCounter()`. All tween properties (ease, delay, repeat, yoyo, callbacks) work. The tween is set to `persist: true` automatically. - -4. **PathFollower offset behavior.** When `positionOnPath: false` (default), the follower's current position becomes the offset from the path start. When `positionOnPath: true`, the follower snaps to the path's start point and the offset is zeroed. - -5. **Ellipse angles are in degrees.** The constructor and `startAngle`/`endAngle` accept degrees (stored internally as radians). The `rotation` property is in radians, but `angle` is in degrees. - -6. **`closePath` vs `autoClose`.** `closePath()` adds an explicit Line curve from end to start. `autoClose = true` only affects `getPoints()` and `getSpacedPoints()` output by appending the first point, without adding a curve. - -7. **Cached lengths can go stale.** `getCurveLengths()` caches results based on array length only. If you modify a curve's control points, call `path.updateArcLengths()` to force recalculation. - -8. **`cubicBezierTo` parameter order with numbers.** When passing numbers: `cubicBezierTo(endX, endY, cp1X, cp1Y, cp2X, cp2Y)` — the end point comes first, not the control points. When passing Vector2 objects: `cubicBezierTo(cp1, cp2, endPoint)`. - -9. **Spline needs at least 4 points.** The Catmull-Rom interpolation works best with 4+ points. With fewer, the curve may not behave as expected. - -10. **Line curve `arcLengthDivisions` is 1.** Unlike other curves (default 100), Line overrides this to 1 since a line is inherently uniform. No need to adjust it. -</gotchas> -</curves_and_paths> diff --git a/skills/base44-platform-games/references/phaser/data_manager.md b/skills/base44-platform-games/references/phaser/data_manager.md deleted file mode 100644 index dc3c832..0000000 --- a/skills/base44-platform-games/references/phaser/data_manager.md +++ /dev/null @@ -1,236 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: data manager - -<data_manager> -Use this when storing runtime key-value data on game objects, scenes, or globally in Phaser 4, listening for data change events, or managing reactive game state. The DataManager provides runtime key-value storage with event-driven change tracking at three levels: per-GameObject (`sprite.setData`/`getData`), per-Scene (`this.data`), and global (`this.registry`). - -<quick_start> -```js -// Per-GameObject data (auto-creates DataManager on first use) -const gem = this.add.sprite(100, 100, 'gem'); -gem.setData('value', 50); -gem.setData({ color: 'red', level: 2 }); -gem.getData('value'); // 50 -gem.getData(['value', 'color']); // [50, 'red'] - -// Increment / toggle helpers -gem.incData('value', 10); // value is now 60 -gem.incData('value', -5); // value is now 55 (negative to decrement) -gem.toggleData('active'); // false -> true (starts from false if unset) - -// Scene-level data (this.data is a DataManagerPlugin) -this.data.set('score', 0); -this.data.get('score'); // 0 -this.data.values.score += 100; // triggers changedata event - -// Global registry (shared across ALL scenes) -this.registry.set('highScore', 9999); -this.registry.get('highScore'); // 9999, readable from any scene -``` -</quick_start> - -<core_concepts> -`Phaser.Data.DataManager` is the base class storing key-value pairs in an internal `list` object. Core methods: `set(key, value)` (stores a value; emits `setdata` for a new key or `changedata` + `changedata-{key}` for an existing one; accepts an object to set multiple keys), `get(key)` (retrieves a value, or pass an array of keys for an array of values), `inc(key, amount)` (increments numeric value, default +1, creates from 0 if missing), `toggle(key)` (flips a boolean, creates from `false` if missing), `remove(key)` (deletes a key, emits `removedata`, accepts an array), `has(key)`, `getAll()` (shallow copy of all pairs as a plain object), `query(regex)` (entries whose keys match a RegExp), `each(callback, context, ...args)` (iterates; callback `(parent, key, value, ...args)`), `merge(data, overwrite)` (bulk import; `overwrite` defaults `true`), `pop(key)` (get and delete in one call), `reset()` (clears all data and unfreezes), `freeze` / `setFreeze(bool)` (when frozen, all writes silently no-op), and `count` (read-only entry count). - -The `values` proxy object allows direct property access with event emission: -```js -// After set('gold', 100), you can do: -data.values.gold += 50; // emits changedata and changedata-gold -// But you MUST use set() to create a key first — direct assignment -// to values for a new key will NOT set up the event proxy. -``` - -`Phaser.Data.DataManagerPlugin` extends DataManager and is registered as the `data` scene plugin (`this.data` in any Scene). It uses the Scene's event emitter (`scene.sys.events`), so data events fire on the Scene's event bus: -```js -this.data.set('lives', 3); -this.events.on('changedata-lives', (scene, value, previousValue) => { - console.log('Lives changed from', previousValue, 'to', value); -}); -``` -The plugin auto-cleans on scene shutdown (removes its shutdown listener) and fully destroys on scene destroy. - -The registry is a plain `DataManager` instance on the `Game` object (`game.registry`), with its own dedicated `EventEmitter` (not shared with any scene). Every scene gets a reference as `this.registry`. It persists for the lifetime of the Game and is never automatically cleared on scene restart or shutdown: -```js -this.registry.set('currentLevel', 1); // Scene A sets -const level = this.registry.get('currentLevel'); // Scene B reads -// Listen for registry changes (events fire on registry.events, NOT this.events) -this.registry.events.on('changedata-currentLevel', (game, value, previousValue) => {}); -``` - -GameObjects do NOT have a DataManager by default — it is created lazily on first call to `setData()`, `getData()`, `incData()`, or `toggleData()` (or explicitly via `setDataEnabled()`). The DataManager's event emitter is the GameObject itself, so data events fire directly on the GameObject: -```js -const player = this.add.sprite(0, 0, 'player'); -player.setData('hp', 100); -player.on('changedata-hp', (gameObject, value, previousValue) => { - if (value <= 0) { gameObject.destroy(); } -}); -``` -</core_concepts> - -<common_patterns> -Setting and getting: -```js -sprite.setData('speed', 200); -sprite.getData('speed'); // 200 -sprite.setData({ speed: 200, direction: 'left', hp: 100 }); // object form -const [speed, hp] = sprite.getData(['speed', 'hp']); // batch get -sprite.data.values.speed = 300; // emits changedata event (after initial set) -``` - -Listening for changes — prefer key-specific listeners for performance: -```js -sprite.on('changedata', (gameObject, key, value, previousValue) => {}); // ANY change -sprite.on('changedata-hp', (gameObject, value, previousValue) => { // SPECIFIC key (preferred) - this.hpBar.setValue(value); -}); -sprite.on('setdata', (gameObject, key, value) => {}); // new key set (first time) -sprite.on('removedata', (gameObject, key, value) => {}); // key removed -``` - -Global registry for cross-scene state: -```js -// HUD scene watches for score changes set by the Game scene -this.registry.events.on('changedata-score', (game, value) => { - this.scoreText.setText('Score: ' + value); -}); -this.events.once('shutdown', () => { - this.registry.events.off('changedata-score'); -}); -// In GameScene.update(): -this.registry.set('score', this.score); -``` - -Complex data and the object-mutation trap: -```js -sprite.setData('inventory', ['sword', 'shield']); -sprite.setData('stats', { str: 10, dex: 8, int: 12 }); - -// CAUTION: mutating a stored object/array does NOT trigger changedata -const inv = sprite.getData('inventory'); -inv.push('potion'); // No event fired! The reference didn't change. -sprite.setData('inventory', [...inv]); // new array reference triggers changedata -``` - -Merging, querying, and freezing: -```js -sprite.data.merge({ hp: 100, speed: 200, armor: 0 }, false); // only sets keys that don't exist -sprite.data.merge(savedState, true); // overwrite existing - -this.data.set('enemy_1_hp', 50); -this.data.set('enemy_2_hp', 80); -this.data.set('player_hp', 100); -const enemyData = this.data.query(/^enemy_/); // { enemy_1_hp: 50, enemy_2_hp: 80 } - -sprite.data.freeze = true; // set, remove, inc, toggle all become no-ops -sprite.setData('hp', 0); // silently ignored -sprite.data.freeze = false; -sprite.data.setFreeze(true); // chainable equivalent -``` -</common_patterns> - -<persistence> -The DataManager, `this.data`, and `this.registry` are runtime state only. They are not persistence and must not be used as storage that survives reloads. - -Only persistence path: define a Base44 entity schema (for example `base44/entities/GameScore.jsonc`) and read/write it with the SDK (`base44.entities.*`) from React (`GamePage.jsx` or the game parent). A Phaser scene must never import `@/api/base44Client` or call the SDK. - -Do not persist game data in `localStorage`, auth profile fields, `this.registry`, `this.data`, module globals, config files, or asset URLs. - -Flow: -1. Phaser emits an `EventBus` event with final/progress data. -2. React handles the event and calls `base44.entities.<Entity>.create/filter/update/delete`. -3. React passes loaded entity data back into Phaser through `EventBus` as runtime state. - -```js -// Phaser scene: emit data, do not import or call the SDK here. -EventBus.emit('score-final', { score: this.score, level: this.level }); - -const onScoresLoaded = (scores) => this.registry.set('leaderboard', scores); -EventBus.on('scores-loaded', onScoresLoaded); -this.events.once('shutdown', () => EventBus.off('scores-loaded', onScoresLoaded)); -``` - -```jsx -// React page/parent: the only persistence layer. -import { base44 } from '@/api/base44Client'; -import { EventBus } from '@/game/EventBus'; - -useEffect(() => { - const saveScore = ({ score, level }) => { - void base44.entities.GameScore.create({ score, level }); - }; - const loadScores = async () => { - const scores = await base44.entities.GameScore.filter({}, '-score', 10); - EventBus.emit('scores-loaded', scores); - }; - - EventBus.on('score-final', saveScore); - void loadScores(); - return () => EventBus.off('score-final', saveScore); -}, []); -``` - -Define the entity schema in the same response as the code that uses it so `base44.entities.<Entity>` exists. Leaderboards use open `read` and owner-scoped writes; never pass owner fields manually. -</persistence> - -<events> -All data events are defined in `Phaser.Data.Events`. The emitter depends on context: for GameObjects it is the GameObject itself; for scene data it is `scene.sys.events` (so `this.events`); for the registry it is `registry.events`. - -- `SET_DATA` (`'setdata'`) — a new key is created. Args `(parent, key, value)`. -- `CHANGE_DATA` (`'changedata'`) — an existing key's value changes. Args `(parent, key, value, previousValue)`. -- `CHANGE_DATA_KEY` (`'changedata-'`) — a specific key changes; append the key name. Args `(parent, value, previousValue)` — note `key` is omitted. -- `REMOVE_DATA` (`'removedata'`) — a key is removed. Args `(parent, key, value)`. -- `DESTROY` (`'destroy'`) — the DataManager's parent is destroyed. No args. - -The `parent` argument is the owner of the DataManager (the GameObject, Scene, or Game instance). `CHANGE_DATA_KEY` is a prefix — the actual event string is `'changedata-'` + the key name (e.g. setting `score` emits `'changedata-score'`). The key-specific `changedata-{key}` fires only for that exact key; the generic `changedata` fires for every key change. -</events> - -<copies_vs_live_references> -Understanding `get()` vs `values` is critical for correct updates: -```js -// get() returns a copy for primitives — modifying the local variable does NOT update the DataManager -let score = this.data.get('score'); -score += 10; // local copy only, DataManager still has the old value - -// values provides a live proxy — assignment triggers CHANGE_DATA event -this.data.values.score += 10; // updates the DataManager AND emits changedata-score - -// For objects, get() returns the stored reference (not a deep copy) -const stats = this.data.get('stats'); -stats.hp -= 10; // mutates the stored object but does NOT trigger events -this.data.set('stats', { ...stats }); // re-set with new reference to trigger event -``` -</copies_vs_live_references> - -<gotchas> -- **Keys are case-sensitive.** `'gold'` and `'Gold'` are two different keys. Be consistent. - -- **`values` proxy requires `set()` first.** You must call `set(key, value)` to create a key before modifying it via `data.values.key`. Direct assignment to `values` for a brand-new key creates a plain property that emits no events. - -- **Object/array mutation does not trigger events.** Mutating a stored object/array in place does not emit `changedata` because the reference has not changed. Re-set the key with a new reference to trigger the event. - -- **Registry listeners persist across scene restarts.** The registry lives on the Game object, so listeners on `this.registry.events` are NOT cleaned up on scene restart. Always remove them on `SHUTDOWN`: -```js -create() { - const handler = (game, value) => { this.updateHUD(value); }; - this.registry.events.on('changedata-score', handler); - this.events.once('shutdown', () => { - this.registry.events.off('changedata-score', handler); - }); -} -``` - -- **Frozen DataManagers fail silently.** When `freeze` is `true`, all writes (`set`, `remove`, `inc`, `toggle`, `pop`, `merge`) silently do nothing — no error, no event. `setFreeze(true)` takes effect immediately, so freezing mid-batch silently no-ops subsequent writes; freeze only after all writes are complete. - -- **Scene Data Plugin shutdown vs destroy.** The plugin removes its shutdown listener on shutdown but does NOT clear data on shutdown — data persists across restart. It only fully resets on scene destroy. For a clean slate on restart, manually `this.data.reset()` in a shutdown listener. - -- **`inc()` on non-numeric values.** `inc()` uses the `+` operator: on strings it concatenates, on booleans they coerce to 0/1. Only use it on keys you know hold numbers. - -- **`reset()` emits no events.** It clears all data silently — no `removedata` fires for the deleted keys. If you need removal events, iterate and call `remove()` per key. - -- **`getAll()` returns a snapshot.** It returns a new plain object each time, a shallow copy — primitives are independent but object/array values still share references with internal storage. - -- **First argument differs by event.** The generic `changedata` callback receives `(parent, key, value, previousValue)`; the key-specific `changedata-{key}` callback omits `key`: `(parent, value, previousValue)`. -</gotchas> -</data_manager> diff --git a/skills/base44-platform-games/references/phaser/events_system.md b/skills/base44-platform-games/references/phaser/events_system.md deleted file mode 100644 index f9f1058..0000000 --- a/skills/base44-platform-games/references/phaser/events_system.md +++ /dev/null @@ -1,193 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: events system - -<events_system> -Use this when working with the Phaser 4 event system: EventEmitter, scene events, game events, custom events, and event-driven communication between systems. Phaser uses the EventEmitter pattern (via eventemitter3) throughout — every major system (Game, Scene, Input, Loader, Cameras, Sound, Tweens, Physics, Textures, Animations) is or contains an EventEmitter. Events use lowercase string keys, and Phaser provides named constants for all built-in events to avoid typos. - -<quick_start> -```js -// on — listen for an event (persists until removed) -this.input.on('pointerdown', (pointer) => { - console.log('clicked at', pointer.x, pointer.y); -}); - -// once — listen for an event, auto-removes after first fire -this.events.once('shutdown', () => { - console.log('scene shutting down'); -}); - -// off — remove a specific listener (must pass same function reference) -const handler = (pointer) => { /* ... */ }; -this.input.on('pointerdown', handler); -this.input.off('pointerdown', handler); - -// emit — fire a custom event with arguments -this.events.emit('player-died', this.player, this.score); - -// removeAllListeners — remove all listeners for an event (or all events) -this.events.removeAllListeners('player-died'); -this.events.removeAllListeners(); // all events -``` - -Always prefer named constants over raw strings to prevent typos: -```js -this.events.on(Phaser.Scenes.Events.UPDATE, (time, delta) => { /* runs every frame */ }); -this.input.on(Phaser.Input.Events.POINTER_DOWN, (pointer) => { /* pointer pressed */ }); -this.game.events.on(Phaser.Core.Events.BLUR, () => { /* browser tab lost focus */ }); -``` -</quick_start> - -<core_concepts> -`Phaser.Events.EventEmitter` extends eventemitter3 and adds `shutdown()` and `destroy()` (both call `removeAllListeners()`). Full inherited API: `on(event, fn, context?)` (persistent listener, returns `this`), `addListener` (alias for `on`), `once(event, fn, context?)` (one-time, auto-removed after first fire), `off(event, fn?, context?, once?)` (remove listener — must pass the same `fn` reference), `removeListener` (alias for `off`), `removeAllListeners(event?)` (remove all for one event, or all events), `emit(event, ...args)` (returns `true` if it had listeners), `listeners(event)`, `listenerCount(event)`, `eventNames()`. - -Every built-in event is a lowercase string exported as a constant; the constant name maps predictably to the string: -```js -Phaser.Scenes.Events.UPDATE // 'update' -Phaser.Scenes.Events.PRE_UPDATE // 'preupdate' -Phaser.Scenes.Events.SHUTDOWN // 'shutdown' -Phaser.Core.Events.BOOT // 'boot' -Phaser.Input.Events.POINTER_DOWN // 'pointerdown' -``` - -Some events use a key-suffix pattern for per-key listening — append the key/type to the prefix constant: -```js -// Loader: listen for a specific file completing — string value: 'filecomplete-image-logo' -this.load.on(Phaser.Loader.Events.FILE_KEY_COMPLETE + 'image-logo', (key, type, data) => {}); -// Animations: listen for a specific animation completing — string value: 'animationcomplete-walk' -sprite.on(Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + 'walk', () => {}); -// Textures: listen for a specific texture being added — string value: 'addtexture-myTexture' -this.textures.on(Phaser.Textures.Events.ADD_KEY + 'myTexture', () => {}); -``` - -The third argument to `on`/`once` sets `this` inside the callback (defaults to the emitter). Arrow functions ignore this argument since they capture lexical `this`: -```js -this.input.on('pointerdown', function (pointer) { - this.cameras.main.shake(100); // 'this' = scene (passed as 3rd arg) -}, this); -this.input.on('pointerdown', (pointer) => { - this.cameras.main.shake(100); // 'this' = enclosing scope (scene in create) -}); -``` -</core_concepts> - -<common_patterns> -Scene lifecycle. Frame loop order: `preupdate` -> `update` -> `Scene.update()` -> `postupdate` -> `prerender` -> `render`. Always clean up on shutdown to prevent leaks on scene restart: -```js -create() { - this.events.on(Phaser.Scenes.Events.UPDATE, this.onUpdate, this); - this.events.on(Phaser.Scenes.Events.SHUTDOWN, () => { - this.events.off(Phaser.Scenes.Events.UPDATE, this.onUpdate, this); - this.input.off('pointerdown', this.onPointerDown, this); - }); -} -``` - -Game-level events fire on the Game instance, shared across all scenes (accessed via `this.game.events`): -```js -this.game.events.on(Phaser.Core.Events.BLUR, this.handleBlur, this); -this.game.events.on(Phaser.Core.Events.VISIBLE, this.handleVisible, this); -``` - -Inter-scene communication has three idioms: -```js -// METHOD 1: game.events — a global event bus accessible from all scenes -this.game.events.emit('score-changed', this.score); // Scene A -this.game.events.on('score-changed', (score) => { this.scoreText.setText(score); }); // Scene B - -// METHOD 2: this.registry — a shared DataManager across all scenes -this.registry.set('score', 100); // Scene A -this.registry.events.on('changedata-score', (parent, value, previousValue) => { // Scene B - this.scoreText.setText(value); -}); - -// METHOD 3: Direct scene access via ScenePlugin -this.scene.get('UIScene').events.emit('update-health', hp); -``` - -Custom events carry arbitrary data arguments: -```js -this.events.emit('player-died', this.player, { lives: this.lives }); -this.events.on('player-died', (player, data) => { - console.log('Lives remaining:', data.lives); -}); -``` - -A standalone EventEmitter can be created for your own buses: -```js -const bus = new Phaser.Events.EventEmitter(); -bus.on('inventory-changed', (items) => { console.log(items.length); }); -bus.emit('inventory-changed', this.inventory); -``` -</common_patterns> - -<event_namespaces> -**Scene Events** (`Phaser.Scenes.Events`) — emitter `this.events`: `BOOT` `'boot'`, `READY` `'ready'`, `START` `'start'`, `CREATE` `'create'`, `PRE_UPDATE` `'preupdate'`, `UPDATE` `'update'`, `POST_UPDATE` `'postupdate'`, `PRE_RENDER` `'prerender'`, `RENDER` `'render'`, `PAUSE` `'pause'`, `RESUME` `'resume'`, `SLEEP` `'sleep'`, `WAKE` `'wake'`, `SHUTDOWN` `'shutdown'` (stopping, may restart), `DESTROY` `'destroy'` (permanent), `ADDED_TO_SCENE` `'addedtoscene'`, `REMOVED_FROM_SCENE` `'removedfromscene'`, plus transition events (`TRANSITION_INIT`/`START`/`OUT`/`COMPLETE`/`WAKE`). - -**Game Events** (`Phaser.Core.Events`) — emitter `this.game.events`: `BOOT` `'boot'`, `READY` `'ready'`, `SYSTEM_READY` `'systemready'`, `PRE_STEP`/`STEP`/`POST_STEP`, `PRE_RENDER`/`POST_RENDER`, `PAUSE`/`RESUME`, `BLUR` `'blur'` (tab lost focus), `FOCUS` `'focus'`, `HIDDEN`/`VISIBLE` (Page Visibility API), `CONTEXT_LOST` `'contextlost'` (WebGL context lost), `DESTROY` `'destroy'`. - -**Input Events** (`Phaser.Input.Events`) — emitter `this.input` (scene-level) or interactive GameObjects. Scene-level pointer: `POINTER_DOWN`/`UP`/`MOVE`/`OVER`/`OUT`, `POINTER_WHEEL` `'wheel'`, `POINTER_DOWN_OUTSIDE`/`UP_OUTSIDE`. Scene-level gameobject: `GAMEOBJECT_DOWN`/`UP`/`MOVE`/`OVER`/`OUT`/`WHEEL`. Per-GameObject (emitted on the GameObject, requires `setInteractive()`): `GAMEOBJECT_POINTER_DOWN` `'pointerdown'` etc. Drag (on `this.input` and on GameObjects): `DRAG_START`/`DRAG`/`DRAG_END`/`DRAG_ENTER`/`DRAG_OVER`/`DRAG_LEAVE`/`DROP`. Other: `GAME_OUT`, `GAME_OVER`, `POINTERLOCK_CHANGE`. - -**Loader Events** (`Phaser.Loader.Events`) — emitter `this.load`: `ADD` `'addfile'`, `START`, `PROGRESS` (0-1), `FILE_LOAD` `'load'`, `FILE_PROGRESS`, `FILE_COMPLETE`, `FILE_KEY_COMPLETE` `'filecomplete-'` (append `type-key`), `FILE_LOAD_ERROR` `'loaderror'`, `POST_PROCESS`, `COMPLETE`. - -**Animation Events** (`Phaser.Animations.Events`) — emitter individual sprites or `this.anims`: `ADD_ANIMATION`, `REMOVE_ANIMATION`, `PAUSE_ALL`/`RESUME_ALL`, `ANIMATION_START`/`RESTART`/`REPEAT`/`UPDATE`/`COMPLETE`/`STOP`, `ANIMATION_COMPLETE_KEY` `'animationcomplete-'` (append key). - -**Camera Events** (`Phaser.Cameras.Scene2D.Events`) — emitter individual camera (e.g. `this.cameras.main`); each effect has a START/COMPLETE pair: `DESTROY`, `FADE_IN_START`/`COMPLETE`, `FADE_OUT_START`/`COMPLETE`, `FLASH_START`/`COMPLETE`, `PAN_START`/`COMPLETE`, `ROTATE_START`/`COMPLETE`, `SHAKE_START`/`COMPLETE`, `ZOOM_START`/`COMPLETE`, `FOLLOW_UPDATE`, `PRE_RENDER`/`POST_RENDER`. - -**Sound Events** (`Phaser.Sound.Events`) — emitter individual sounds or `this.sound`. Per-sound: `PLAY`/`PAUSE`/`RESUME`/`STOP`/`COMPLETE`/`LOOP`/`LOOPED`/`SEEK`/`MUTE`/`VOLUME`/`RATE`/`DETUNE`/`PAN`/`DECODED`/`DESTROY`. SoundManager-level (on `this.sound`): `GLOBAL_MUTE`/`GLOBAL_VOLUME`/`GLOBAL_RATE`/`GLOBAL_DETUNE`, `PAUSE_ALL`/`RESUME_ALL`/`STOP_ALL`, `DECODED_ALL`, `UNLOCKED`. - -**Tween Events** (`Phaser.Tweens.Events`) — emitter individual tweens: `TWEEN_ACTIVE`/`START`/`UPDATE`/`YOYO`/`REPEAT`/`LOOP`/`PAUSE`/`RESUME`/`COMPLETE`/`STOP`. - -**Arcade Physics Events** (`Phaser.Physics.Arcade.Events`) — emitter `this.physics.world`: `COLLIDE`, `OVERLAP`, `TILE_COLLIDE`, `TILE_OVERLAP`, `WORLD_BOUNDS`, `WORLD_STEP`, `PAUSE`, `RESUME`. - -**Texture Events** (`Phaser.Textures.Events`) — emitter `this.textures`: `ADD` `'addtexture'`, `ADD_KEY` `'addtexture-'` (append key), `REMOVE`/`REMOVE_KEY`, `LOAD` `'onload'`, `ERROR` `'onerror'`, `READY`. - -**GameObject Events** (`Phaser.GameObjects.Events`) — emitter individual GameObjects: `ADDED_TO_SCENE`, `REMOVED_FROM_SCENE`, `DESTROY`. Video GameObjects also emit `VIDEO_PLAY`/`PLAYING`/`COMPLETE`/`LOOP`/`STOP`/`CREATED`/`ERROR`/`LOCKED`/`UNLOCKED`/`METADATA`/`SEEKED`/`SEEKING`/`STALLED`/`TEXTURE`/`UNSUPPORTED`. - -**Time Events** (`Phaser.Time.Events`) — emitter `Phaser.Time.TimerEvent` instances: `COMPLETE` `'complete'` (TimerEvent finishes all repeats). -</event_namespaces> - -<gotchas> -- **Memory leaks from unremoved listeners** — the most common bug. `on()` does not auto-remove, so if a scene restarts via `scene.restart()`, old listeners persist and each restart adds duplicates. Always clean up in a `SHUTDOWN` handler, or use `once()` for events you only need fired once: -```js -// BAD: leaks listeners on every scene restart -create() { this.input.on('pointerdown', this.shoot, this); } - -// GOOD: clean up in shutdown -create() { - this.input.on('pointerdown', this.shoot, this); - this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { - this.input.off('pointerdown', this.shoot, this); - }); -} -``` - -- **shutdown vs destroy.** `SHUTDOWN` fires when a scene stops but can restart later — clean up listeners here. `DESTROY` fires when a scene is permanently removed — use for final cleanup. A scene restart fires `SHUTDOWN` then `START` then `CREATE`; it does NOT fire `DESTROY`. - -- **Context binding.** The third argument to `on`/`once` sets `this` inside the callback. Without it, `this` defaults to the emitter, not the scene. Pass `this` with regular functions, or use arrow functions (which capture lexical `this`). - -- **`off()` requires exact references.** It only works if you pass the exact same function reference (and context) used with `on()`. Anonymous or inline arrow functions cannot be removed — store a reference or use a class method: -```js -// BAD: arrow function cannot be removed later -this.events.on('update', () => { this.doStuff(); }); -// GOOD: named method can be removed -this.events.on('update', this.onUpdate, this); -this.events.off('update', this.onUpdate, this); -``` - -- **`once()` auto-removes** the listener after first fire — no manual cleanup needed. - -- **Input event hierarchy.** Input events fire in order: (1) `GAMEOBJECT_POINTER_DOWN` on the GameObject, (2) `GAMEOBJECT_DOWN` on `this.input`, (3) `POINTER_DOWN` on `this.input`. Higher handlers can stop propagation. - -- **Game events vs scene events.** `this.game.events` and `this.events` are different emitters. Game events fire once per game loop tick across all scenes; scene events fire per-scene. Listeners on `game.events` persist across scene restarts — always clean them up on `SHUTDOWN`: -```js -create() { - this.game.events.on(Phaser.Core.Events.BLUR, this.onBlur, this); - this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { - this.game.events.off(Phaser.Core.Events.BLUR, this.onBlur, this); - }); -} -``` -</gotchas> -</events_system> diff --git a/skills/base44-platform-games/references/phaser/filters_and_postfx.md b/skills/base44-platform-games/references/phaser/filters_and_postfx.md deleted file mode 100644 index 5e5a00c..0000000 --- a/skills/base44-platform-games/references/phaser/filters_and_postfx.md +++ /dev/null @@ -1,198 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: filters and postfx - -<filters_and_postfx> -Use this when applying visual filters or post-processing effects in Phaser 4: bloom, blur, glow, color matrix, barrel distortion, displacement, masks, custom shaders, and the filter pipeline. Filters are GPU-based, WebGL-only effects applied after an object or camera renders to a texture. - -<quick_start> -```js -// Add a glow to a sprite -const sprite = this.add.sprite(400, 300, 'player'); -sprite.enableFilters(); // Step 1: enable filters (WebGL only) -sprite.filters.internal.addGlow(0xff00ff, 4, 0, 1); // Step 2: add via .filters.internal or .filters.external - -// Add a blur to the camera (cameras have filters enabled by default) -const camera = this.cameras.main; -camera.filters.internal.addBlur(0, 2, 2, 1); -``` -</quick_start> - -<core_concepts> -Each filter runs a shader pass over the rendered texture, producing the final visual output. The rendering pipeline for a camera with filters: (1) objects render to a texture the size of the camera; (2) **internal filters** process that texture in object/camera local space; (3) the texture is drawn to a context-sized texture, applying camera transforms (position, rotation, zoom); (4) **external filters** process that context texture in screen space; (5) the final texture is composited into the output. - -Every `FilterList` exposes two sub-lists, `filters.internal` and `filters.external`, controlling **when** the filter runs relative to the camera/object transform: -- **Internal** — applied before the camera transform, operating in the object's local coordinate space. A horizontal blur on a rotated object appears rotated with the object. Internal filters only cover the object/camera region, so they are cheaper. Use internal wherever possible. -- **External** — applied after the camera transform, operating in screen space. A horizontal blur on a rotated object always blurs horizontally on screen. External filters are full-screen and more expensive. - -`FilterList` (`Phaser.GameObjects.Components.FilterList`) is the container holding filter controllers. It provides `add(filter, index)` (add a Controller at an optional index), `remove(filter, forceDestroy)`, `clear()` (remove and destroy all), `getActive()` (all filters where `active === true`), `list` (raw array of Controllers, safe to reorder), and convenience factories like `addBlur()`, `addGlow()`, `addMask()`. - -Every filter is a `Phaser.Filters.Controller` subclass. Common properties: `active` (toggle on/off without removing), `camera` (the owning camera), `renderNode` (shader render node ID), `paddingOverride` (override automatic padding), `ignoreDestroy` (if true the filter survives when its FilterList is destroyed, for reuse). Key methods: `setActive(bool)`, `setPaddingOverride(left, top, right, bottom)`, `getPadding()`, `destroy()`. - -Cameras have filters by default; game objects do not — you must call `enableFilters()` first: -```js -const sprite = this.add.sprite(400, 300, 'hero'); -sprite.enableFilters(); // creates an internal filterCamera, returns this for chaining -sprite.filters.internal.addGlow(); -sprite.filters.external.addVignette(); -``` -After enabling, related game-object properties: `filterCamera` (the internal camera used for filter rendering), `filters` ({internal, external} FilterList pair), `renderFilters` (master toggle, default true), `filtersAutoFocus` (auto-adjust camera to follow the object, default true), `filtersFocusContext` (focus the rendering context instead of object bounds, default false), `filtersForceComposite` (always draw to a framebuffer even with no active filters, default false), `maxFilterSize` (max texture size for filter framebuffers). Use `willRenderFilters()` to check if any active filters will actually render. -</core_concepts> - -<common_patterns> -Adding and modifying filters on game objects: -```js -const sprite = this.add.sprite(400, 300, 'enemy'); -sprite.enableFilters(); - -const glow = sprite.filters.internal.addGlow(0x00ff00, 4); -glow.outerStrength = 8; // modify at runtime -glow.color = 0xff0000; -glow.setActive(false); // temporarily disable -sprite.filters.internal.remove(glow); // remove and destroy -``` - -Camera filters: -```js -const camera = this.cameras.main; -const blur = camera.filters.internal.addBlur(0, 2, 2, 1); // camera-local space -const vignette = camera.filters.external.addVignette(0.5, 0.5, 0.5, 0.5); // screen space -const cm = camera.filters.internal.addColorMatrix(); -cm.colorMatrix.sepia(); // color grading -``` - -Chaining — filters execute in list order; each receives the output of the previous one: -```js -const cam = this.cameras.main; -const cm = cam.filters.internal.addColorMatrix(); -cm.colorMatrix.brightness(0.2); // first: color grading -cam.filters.internal.addBlur(1, 2, 2, 1); // second: blur the color-graded result -cam.filters.external.addVignette(0.5, 0.5, 0.5, 0.8); // third: vignette on top -``` - -Masks in v4 are implemented as filters — they use a texture's or game object's alpha channel to control visibility. Internal masks match the object being filtered; external masks match the camera context. Use a `viewCamera` parameter to control which camera renders the mask game object: -```js -sprite.enableFilters(); -sprite.filters.internal.addMask('maskTexture'); // static texture - -const maskShape = this.add.circle(0, 0, 100, 0xffffff); -const mask = sprite.filters.internal.addMask(maskShape); // game object (renders to DynamicTexture) -mask.invert = true; -mask.autoUpdate = true; // default: re-renders each frame -mask.needsUpdate = true; // force a one-time update -sprite.filters.external.addMask(maskShape, false, this.cameras.main); // specific viewing camera -``` - -Wipe / reveal transitions — animate `progress` via tween: -```js -const wipe = camera.filters.external.addWipe(0.1, 0, 0); -this.tweens.add({ targets: wipe, progress: 1, duration: 2000, ease: 'Linear' }); -wipe.setLeftToRight(); -wipe.setTopToBottom(); -wipe.setRevealEffect(); // reveal mode -wipe.setWipeEffect(); // wipe mode -wipe.setTexture('nextSceneCapture'); // wipe to another texture (scene transitions) -``` - -ParallelFilters splits the input into two paths, processes each independently, then blends — this replaces the dedicated Bloom filter from v3: -```js -const pf = camera.filters.internal.addParallelFilters(); -pf.top.addThreshold(0.5, 1); // top path: threshold bright areas... -pf.top.addBlur(); // ...then blur them -pf.blend.blendMode = Phaser.BlendModes.ADD; // configure how top combines onto bottom -pf.blend.amount = 0.5; -// Bottom path: left empty = uses original input -``` - -CaptureFrame captures the current render state at the point it appears in the display list — objects rendered before it are captured, objects after are not (requires composite mode on the camera): -```js -this.cameras.main.setForceComposite(true); -const bg = this.add.image(400, 300, 'background'); // rendered before CaptureFrame -> captured -const capture = this.add.captureFrame('myCapture'); -const display = this.add.image(400, 300, 'myCapture'); // display the captured texture with filters -display.enableFilters(); -display.filters.internal.addBlur(0, 4, 4, 2); -``` -</common_patterns> - -<built_in_filters> -All accessible via FilterList factory methods (`addX(...)`): -- **Barrel** `addBarrel(amount)` — pinch/expand distortion; `amount=1` is neutral. -- **Blend** `addBlend(texture, blendMode, amount, color)` — blend another texture using a blend mode (supports modes not available in standard WebGL). -- **Blocky** `addBlocky(config)` — pixelation that preserves original colors (no blending); best without anti-aliasing. -- **Blur** `addBlur(quality, x, y, strength, color, steps)` — Gaussian blur; quality 0=low, 1=medium, 2=high. -- **Bokeh** `addBokeh(radius, amount, contrast)` — depth-of-field bokeh blur. -- **ColorMatrix** `addColorMatrix()` — color manipulation via matrix; access `.colorMatrix` for presets. -- **CombineColorMatrix** `addCombineColorMatrix(texture)` — combine channels from two textures via color matrices (e.g. alpha transfer). -- **Displacement** `addDisplacement(texture, x, y)` — pixel displacement via a displacement map; values are very small floats (e.g. 0.005). -- **Glow** `addGlow(color, outerStrength, innerStrength, scale, knockout, quality, distance)` — luminous halo around edges; supports inner/outer glow and knockout mode. -- **GradientMap** `addGradientMap(config)` — recolor image using a ColorRamp based on brightness. -- **ImageLight** `addImageLight(config)` — image-based lighting using a panorama environment map and normal map. -- **Key** `addKey(config)` — chroma key: remove or isolate a color. Config `{ color, threshold, feather, isolate }`. -- **Mask** `addMask(mask, invert, viewCamera, viewTransform, scaleFactor)` — alpha masking via texture or game object. -- **NormalTools** `addNormalTools(config)` — manipulate normal maps: rotate, adjust facing power, output grayscale facing data. -- **PanoramaBlur** `addPanoramaBlur(config)` — spherically-correct blur for panorama images (for use with ImageLight; very slow). -- **ParallelFilters** `addParallelFilters()` — split input into two filter paths, blend results; use for custom bloom. -- **Pixelate** `addPixelate(amount)` — mosaic/pixelation; pixel size = 2 + amount; blends colors (unlike Blocky). -- **Quantize** `addQuantize(config)` — reduce color palette; supports RGBA/HSVA modes, gamma, offset, dithering. -- **Sampler** `addSampler(callback, region)` — extract pixel data from the render; does not alter the image; expensive. -- **Shadow** `addShadow(x, y, decay, power, color, samples, intensity)` — drop shadow with offset, decay, and color. -- **Threshold** `addThreshold(edge1, edge2, invert)` — binary threshold per channel; edges can be arrays for per-channel control. -- **TiltShift** `addTiltShift(radius, amount, contrast, blurX, blurY, strength)` — miniature/tilt-shift effect (uses Bokeh internally). -- **Vignette** `addVignette(x, y, radius, strength, color, blendMode)` — edge darkening/coloring; supports NORMAL, ADD, MULTIPLY, SCREEN blend modes. -- **Wipe** `addWipe(wipeWidth, direction, axis, reveal, wipeTexture)` — wipe/reveal transition; animate `progress` via tween. - -Mask filter API: -```js -const mask = list.addMask('texKey'); // from texture key -const mask = list.addMask(gameObject); // from game object -mask.invert = true; -mask.autoUpdate = false; // stop auto-updating GO masks -mask.needsUpdate = true; // force one update -mask.setTexture('newKey'); -mask.setGameObject(newGO); -mask.viewCamera = otherCamera; // camera for GO rendering -mask.viewTransform = 'local'; // 'local' or 'world' -mask.scaleFactor = 0.5; // scale mask texture size -``` - -ColorMatrix presets (call on `addColorMatrix().colorMatrix`): `sepia()`, `grayscale(1)`, `brightness(0.3)`, `hue(90)`, `saturate(-0.5)`, `contrast(0.3)`, `blackWhite()`, `negative()`, `desaturate()`, `night(0.5)`, `lsd()`, `brown()`, `vintagePinhole()`, `kodachrome()`, `technicolor()`, `polaroid()`, `shiftToBGR()`. -</built_in_filters> - -<gotchas> -1. **WebGL only.** Filters do not work in the Canvas renderer. `enableFilters()` returns early if WebGL is unavailable. - -2. **`enableFilters()` required for game objects.** Cameras have filters by default; sprites, images, containers, and other game objects require `enableFilters()` before accessing `filters`. - -3. **Performance cost.** Each object with active filters creates extra draw calls (one for the base render plus one per active filter). Use sparingly and performance test early. - -4. **Internal vs external matters.** Internal filters are cheaper (object-region sized); external filters are full-screen. A blur that should rotate with the object must be internal; a blur that should stay screen-aligned must be external. - -5. **Filter order matters.** Filters are applied sequentially in list order — the output of one feeds into the next. - -6. **Glow quality and distance are immutable.** `quality` and `distance` on the Glow filter cannot be changed after creation. Destroy and recreate the filter to change them. - -7. **CaptureFrame requires forceComposite.** The camera must have `setForceComposite(true)` or otherwise render into a framebuffer for CaptureFrame to work. - -8. **Padding for expanding effects.** Blur, Glow, and Shadow can automatically calculate padding to expand the render texture. Override with `setPaddingOverride()` (pass `null` to clear). On a camera, use `camera.getPaddingWrapper(x)` to render more world outside the image edge. - -9. **Controller reuse.** By default controllers are destroyed when their FilterList is destroyed. Set `ignoreDestroy = true` to reuse a controller across multiple objects, but then you must manage its lifecycle manually. Works best with external filters. - -10. **Mask game object rendering.** When using a game object as a mask source, it renders to a DynamicTexture each frame (if `autoUpdate` is true). For static masks, set `autoUpdate = false` and use `needsUpdate = true` for one-shot updates to improve performance. - -11. **No Bloom filter.** v4 has no dedicated Bloom filter. Use ParallelFilters with Threshold + Blur + ADD blend (see Common Patterns), or use `Phaser.Actions.AddEffectBloom` to automate it. -</gotchas> - -<v4_changes_from_v3> -- `gameObject.preFX` / `gameObject.postFX` → `gameObject.filters.internal` / `gameObject.filters.external`. -- `camera.postFX` → `camera.filters.internal` / `camera.filters.external` (cameras now have both lists). -- `FX.addBloom()` → use ParallelFilters + Threshold + Blur, or `Phaser.Actions.AddEffectBloom`. No dedicated Bloom filter. -- `FX.addCircle()` → use Vignette (with radius), a circular Mask, or `Phaser.Actions.AddMaskShape`. -- `FX.addGradient()` → use the new Gradient GameObject; add Quantize for steps if wanted. -- Glow `quality` was a 0-1 fraction; it is now an integer (default 10), with stochastic sampling replacing line sampling (higher quality at lower values). -- `camera.setMask()` → `camera.filters.internal.addMask()`; `gameObject.setMask()` → `gameObject.filters.internal.addMask()`. Masks are now filters, not a separate system. -- FX controllers → `Phaser.Filters.Controller` subclasses (same pattern: returned controller objects with mutable properties). -- New in v4: `enableFilters()` is now a required explicit opt-in for game objects; new filters Blocky, Quantize, Key, Blend, CombineColorMatrix, ImageLight, NormalTools, PanoramaBlur, ParallelFilters, Sampler. -</v4_changes_from_v3> -</filters_and_postfx> diff --git a/skills/base44-platform-games/references/phaser/game_object_components.md b/skills/base44-platform-games/references/phaser/game_object_components.md deleted file mode 100644 index 1bfb175..0000000 --- a/skills/base44-platform-games/references/phaser/game_object_components.md +++ /dev/null @@ -1,158 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: game object components - -<game_object_components> -Use this when working with Phaser 4 game object components and the mixin system: Transform, Alpha, Tint, Origin, Depth, Flip, Mask, GetBounds, Lighting, ScrollFactor, Size, Texture, Visible, PathFollower, and the v4 filter components. - -<quick_start> -Every Game Object gets its component methods via mixins — just call them directly on any game object: - -```js -const sprite = this.add.sprite(400, 300, 'hero'); - -sprite.setAlpha(0.5); // Alpha -sprite.setBlendMode(Phaser.BlendModes.ADD); // BlendMode -sprite.setDepth(10); // Depth -sprite.setFlip(true, false); // Flip -sprite.setOrigin(0, 0); // Origin -sprite.setScrollFactor(0); // ScrollFactor (HUD) -sprite.setDisplaySize(64, 64); // Size -sprite.setTexture('hero', 'walk-1'); // Texture -sprite.setTint(0xff0000); // Tint -sprite.setPosition(100, 200); // Transform -sprite.setVisible(false); // Visible -sprite.setScale(2); // Transform -sprite.setAngle(45); // Transform - -sprite.setAlpha(1, 0.5, 0.5, 1); // per-corner alpha (WebGL only) -sprite.setCrop(0, 0, 32, 32); // crop a texture region - -// Enable WebGL filters (post-processing) -sprite.enableFilters(); -sprite.filters.internal.addBlur(1, 2, 2, 1); - -sprite.setLighting(true); // Lighting (WebGL only, v4 feature) -``` -</quick_start> - -<core_concepts> -Phaser uses `Phaser.Class` with a `Mixins` array to compose Game Objects from reusable component objects. Each component is a plain JS object whose properties and methods are copied onto the class prototype at class-definition time — this is property copying, NOT prototypal inheritance. Components are mixed in per-class, not per-instance. - -```js -// How Phaser defines Sprite internally: -var Sprite = new Class({ - Extends: GameObject, - Mixins: [ - Components.Alpha, Components.BlendMode, Components.Depth, - Components.Flip, Components.GetBounds, Components.Lighting, - Components.Mask, Components.Origin, Components.RenderNodes, - Components.ScrollFactor, Components.Size, Components.TextureCrop, - Components.Tint, Components.Transform, Components.Visible, - SpriteRender - ], - initialize: function Sprite (scene, x, y, texture, frame) { /* ... */ } -}); -``` - -Key rules: -- Most setter methods return `this` for chaining: `sprite.setAlpha(0.5).setDepth(10).setPosition(100, 200)`. -- Many properties use getters/setters internally (`alpha`, `depth`, `visible`, `scale`, `rotation`). Setting `alpha = 0` or `scale = 0` clears render flags, skipping rendering entirely. -- `renderFlags` is a bitmask: bit 0 = visible, bit 1 = alpha, bit 2 = scale, bit 3 = texture. All bits must be set for the object to render. -- **Alpha vs AlphaSingle**: `Alpha` supports per-corner alpha (WebGL); `AlphaSingle` only supports a single global alpha. Containers use `AlphaSingle`. -- **Texture vs TextureCrop**: `TextureCrop` extends `Texture` with `setCrop()` support. Sprites and Images use `TextureCrop`; some objects use plain `Texture`. -- **Filters and RenderSteps** are v4-only, WebGL-only systems for post-processing. -</core_concepts> - -<components> -**Alpha** — per-corner transparency (Sprite, Image, Text, most renderables). `alpha` (0-1, setting also sets all four corners; 0 disables rendering), `alphaTopLeft`/`alphaTopRight`/`alphaBottomLeft`/`alphaBottomRight` (WebGL only), `setAlpha(topLeft?, topRight?, bottomLeft?, bottomRight?)` (one arg = uniform, four = per-corner), `clearAlpha()` (resets to 1). - -**AlphaSingle** — simplified alpha, no per-corner support (used by Container). `alpha` (0-1), `setAlpha(value?)`, `clearAlpha()`. - -**BlendMode** — how pixels composite. `blendMode` (accepts `Phaser.BlendModes` const, string, or integer), `setBlendMode(value)`. WebGL modes: `NORMAL`, `ADD`, `MULTIPLY`, `SCREEN`, `ERASE`. Canvas supports additional browser-dependent modes. - -**Depth** — rendering order (z-index); higher renders on top. `depth` (setting queues a depth sort, default 0), `setDepth(value)`, `setToTop()`, `setToBack()`, `setAbove(gameObject)`, `setBelow(gameObject)`. - -**Flip** — visual mirroring without affecting physics bodies. `flipX`/`flipY` (default false), `setFlipX(value)`, `setFlipY(value)`, `setFlip(x, y)`, `toggleFlipX()`, `toggleFlipY()`, `resetFlip()`. - -**GetBounds** — positions and bounding rects regardless of origin. `getCenter`, `getTopLeft`, `getTopCenter`, `getTopRight`, `getLeftCenter`, `getRightCenter`, `getBottomLeft`, `getBottomCenter`, `getBottomRight` (all `(output?, includeParent?)` returning `Vector2Like`), `getBounds(output?)` returning `Rectangle`. Pass `includeParent = true` to factor in parent Container transforms. - -**Lighting** (v4, WebGL only) — light-based rendering. `lighting` (default false), `selfShadow` (`{ enabled, penumbra, diffuseFlatThreshold }`), `setLighting(enable)`, `setSelfShadow(enabled?, penumbra?, diffuseFlatThreshold?)` (pass `null` for enabled to use game config default). - -**Mask** (Canvas only in v4) — geometry masking for the Canvas renderer; in WebGL use `Filters.addMask()` instead. `mask` (current GeometryMask or null), `setMask(mask)` (Canvas only; logs a warning in WebGL), `clearMask(destroyMask?)`, `createGeometryMask(graphics?)`. - -**Origin** — anchor point for position/rotation/scale, normalized 0-1. `originX`/`originY` (default 0.5 = center), `displayOriginX`/`displayOriginY` (pixels; setting recalculates normalized origin), `setOrigin(x?, y?)` (y defaults to x), `setOriginFromFrame()`, `setDisplayOrigin(x?, y?)`, `updateDisplayOrigin()`. - -**ScrollFactor** — how camera scrolling affects rendered position. `scrollFactorX`/`scrollFactorY` (default 1), `setScrollFactor(x, y?)`. Key values: `0` = fixed to camera (HUD), `1` = moves with camera, `0.5` = parallax half-speed. - -**Size** — native and display dimensions. `width`/`height` (native, un-scaled), `displayWidth`/`displayHeight` (scaled; setting adjusts scaleX/scaleY), `setSize(width, height)` (native size, does NOT affect rendered size), `setDisplaySize(width, height)` (rendered size via scale), `setSizeToFrame(frame?)`. - -**Texture** — texture and frame for rendering. `texture`, `frame`, `setTexture(key, frame?, updateSize?, updateOrigin?)`, `setFrame(frame, updateSize?, updateOrigin?)` (name, index, or Frame instance; updates size and origin by default). - -**TextureCrop** — Texture plus cropping (Sprite, Image). Adds `isCropped` and `setCrop(x?, y?, width?, height?)` (no args clears; accepts a `Rectangle` as first arg). `setFrame` also updates crop UVs when cropped. - -**Tint** (WebGL only) — color tinting via per-corner vertex colors. `tint` (uniform, default 0xffffff), `tintTopLeft`/`tintTopRight`/`tintBottomLeft`/`tintBottomRight`, `tintMode` (default `Phaser.TintModes.MULTIPLY`), `isTinted` (readonly), `setTint(topLeft?, topRight?, bottomLeft?, bottomRight?)`, `setTintMode(mode)` (`MULTIPLY`, `FILL`, `ADD`, `SCREEN`, `OVERLAY`, `HARD_LIGHT`), `clearTint()`. v4 change: `setTintFill()` is removed — use `setTint(color).setTintMode(Phaser.TintModes.FILL)`. - -**Transform** — position, scale, rotation, coordinate transforms. `x`/`y`/`z`/`w` (`z` is NOT rendering order — use `depth`), `scale` (get returns average of scaleX/scaleY), `scaleX`/`scaleY` (default 1), `angle` (degrees, clockwise, 0 = right), `rotation` (radians). Methods: `setPosition(x?, y?, z?, w?)`, `copyPosition(source)`, `setRandomPosition(x?, y?, width?, height?)`, `setRotation(radians?)`, `setAngle(degrees?)`, `setScale(x?, y?)`, `setX`/`setY`/`setZ`/`setW`, `getLocalTransformMatrix(tempMatrix?)`, `getWorldTransformMatrix(tempMatrix?, parentMatrix?)`, `getLocalPoint(x, y, point?, camera?)`, `getWorldPoint(point?, tempMatrix?, parentMatrix?)`, `getParentRotation()`. - -**Visible** — whether the object renders (invisible objects still run update logic). `visible` (default true), `setVisible(value)`. - -**PathFollower** — follow a `Phaser.Curves.Path` via an internal tween. `path`, `rotateToPath` (default false), `pathRotationOffset`, `pathOffset`, `pathVector`, `pathDelta`, `pathTween`. Methods: `setPath(path, config?)`, `setRotateToPath(value, offset?)`, `isFollowing()`, `startFollow(config?, startAt?)` (config can be a duration number or a PathConfig), `pauseFollow()`, `resumeFollow()`, `stopFollow()`. - -**RenderNodes, RenderSteps, Filters, FilterList** (v4, WebGL only) — call `enableFilters()` on game objects before accessing `filters`. Cameras have filters by default. Use `filters.internal` for object-local effects and `filters.external` for screen-space effects. -</components> - -<factory_and_display_list> -Register custom game objects on the factory so they can be created via `this.add.myObject()`: - -```js -class LaserBeam extends Phaser.GameObjects.Sprite { - constructor(scene, x, y) { super(scene, x, y, 'laser'); } -} - -Phaser.GameObjects.GameObjectFactory.register('laserBeam', function (x, y) { - const beam = new LaserBeam(this.scene, x, y); - this.displayList.add(beam); - return beam; -}); - -const laser = this.add.laserBeam(100, 200); // usable in any scene -``` - -**Factory (`this.add`) vs Creator (`this.make`)**: `this.add` creates AND adds to the display list. `this.make` creates but does NOT add — useful for off-screen prep or deferred display. - -```js -const visible = this.add.sprite(100, 100, 'hero'); // on display list -const offscreen = this.make.sprite({ key: 'hero', x: 100, y: 100 }); // not added -this.add.existing(offscreen); // add later -``` - -`GetAdvancedValue` resolves static values, random arrays, randInt/randFloat ranges, and callbacks from config objects: - -```js -{ speed: 100 } // static value -{ speed: [100, 200, 300] } // random from array -{ speed: { randInt: [50, 150] } } // random integer range -{ speed: { randFloat: [0.5, 1.5] } } // random float range -{ speed: function () { return Math.random() * 100; } } // callback -``` - -Display list ordering: objects added later render on top. Control with `setDepth(10)` (higher = on top, triggers a sort) or display-list position (`setToTop()`, `setToBack()`, `setAbove(otherSprite)`, `setBelow(otherSprite)`). When both are used, `depth` takes priority after the next sort pass. -</factory_and_display_list> - -<gotchas> -- **alpha=0 prevents rendering entirely**: setting `alpha` to 0 clears a render flag bit — the object is not drawn at all, not merely transparent. Same for `scale = 0` and `visible = false`. -- **Flip does not affect physics**: flipping is rendering-only; physics bodies remain unchanged. Account for this in collision code. -- **ScrollFactor and physics**: collisions use world position. A scroll factor other than 1 offsets where the texture renders but not where the body is. For HUD elements with scroll factor 0, avoid adding physics bodies. -- **depth vs display list order**: `setDepth()` queues a sort and is the primary z-ordering tool. `setToTop()`/`setAbove()`/etc. reorder the display list without changing `depth`. If you mix both, `depth` wins after the next sort. -- **Mask component is Canvas-only in v4**: for WebGL masking, use `enableFilters()` then `filters.internal.addMask()`. `setMask()` logs a warning in WebGL. -- **setTintFill is removed in v4**: use `setTint(color).setTintMode(Phaser.TintModes.FILL)`. -- **Tint mode is separate from tint color in v4**: modes are `MULTIPLY` (default), `FILL`, `ADD`, `SCREEN`, `OVERLAY`, `HARD_LIGHT`. -- **displayWidth/displayHeight adjusts scale**: setting them modifies `scaleX`/`scaleY`, NOT `width`/`height`. -- **setSize vs setDisplaySize**: `setSize()` changes internal dimensions (frames, physics); `setDisplaySize()` changes rendered size by adjusting scale. -- **Origin default is 0.5**: all Game Objects default to center origin. Use `setOrigin(0, 0)` for top-left positioning. Text and BitmapText instead default to top-left. -- **Transform.z is NOT depth**: `z` is for custom use. Rendering order is `depth` or display list position. -- **Filters are expensive**: each object with active filters needs extra draw calls. Use `renderFilters = false` to temporarily disable. Internal filters are cheaper than external. -</gotchas> -</game_object_components> diff --git a/skills/base44-platform-games/references/phaser/game_setup_and_config.md b/skills/base44-platform-games/references/phaser/game_setup_and_config.md deleted file mode 100644 index 2a5ab5a..0000000 --- a/skills/base44-platform-games/references/phaser/game_setup_and_config.md +++ /dev/null @@ -1,233 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: game setup and config - -<game_setup_and_config> -Use this when creating a new Phaser 4 game instance or configuring GameConfig options: renderer selection, canvas setup, scaling, pixel art, FPS settings, boot sequence, and config sub-objects. - -<quick_start> -The simplest possible Phaser 4 game — a single scene with the default 1024×768 canvas: - -```js -class MyScene extends Phaser.Scene { - preload() { this.load.image('logo', 'assets/logo.png'); } - create() { this.add.image(400, 300, 'logo'); } -} - -const config = { - type: Phaser.AUTO, - scene: MyScene -}; - -const game = new Phaser.Game(config); -``` - -`new Phaser.Game(config)` triggers the entire boot sequence: config parsing, renderer creation, DOM insertion, and the game loop (`TimeStep`). The game waits for `DOMContentLoaded` before booting. -</quick_start> - -<core_concepts> -**Boot sequence**: `new Phaser.Game(config)` parses config into a `Phaser.Core.Config` instance, then creates the global managers (`AnimationManager`, `TextureManager`, `CacheManager`, `InputManager`, `SceneManager`, `ScaleManager`, `SoundManager`, `TimeStep`, `PluginManager`). After `DOMContentLoaded` it calls `boot()` — which creates the renderer, adds the canvas to the DOM, prints the debug header, and emits `BOOT`. Once textures are ready (`TextureManager` emits `READY`), it emits `READY` and calls `start()`, which begins the `TimeStep` loop, sets up the `VisibilityHandler`, and calls `config.postBoot`. - -**Config parsing**: the `Config` constructor reads a flat `GameConfig` object and resolves defaults. Some properties can be specified at top level OR nested in sub-objects (e.g. `width` can be top-level or under `scale.width`; `pixelArt` can be top-level or under `render`). The nested sub-object (`scale`, `render`) takes priority when both are present. - -**Renderer constants** (set via `config.type`, default `Phaser.AUTO`): -- `Phaser.AUTO` (0) — WebGL if supported, else falls back to Canvas. -- `Phaser.CANVAS` (1) — force Canvas renderer. -- `Phaser.WEBGL` (2) — force WebGL, no fallback if unsupported. -- `Phaser.HEADLESS` (3) — no renderer (DOM still required). For unit testing only. -</core_concepts> - -<common_patterns> -Pixel art game — when `pixelArt` is `true`, Config automatically sets `antialias: false`, `antialiasGL: false`, and `roundPixels: true`: - -```js -const config = { - type: Phaser.AUTO, - width: 320, - height: 240, - pixelArt: true, - scale: { - mode: Phaser.Scale.FIT, - autoCenter: Phaser.Scale.CENTER_BOTH, - zoom: Phaser.Scale.ZOOM_2X - }, - scene: MyScene -}; -``` - -Smooth pixel art (WebGL only) — preserves blocky pixels but smooths edges between them when scaled up. When `smoothPixelArt` is `true`, Config sets `antialias: true`, `antialiasGL: true`, and `pixelArt: false`: - -```js -const config = { - type: Phaser.WEBGL, - width: 320, - height: 240, - smoothPixelArt: true, - scene: MyScene -}; -``` - -Full-window responsive game: - -```js -const config = { - type: Phaser.AUTO, - scale: { - mode: Phaser.Scale.RESIZE, - parent: 'game-container', - width: '100%', - height: '100%' - }, - scene: MyScene -}; -``` - -Fixed aspect ratio with FIT (letterboxing): - -```js -const config = { - type: Phaser.AUTO, - scale: { - mode: Phaser.Scale.FIT, - parent: 'game-container', - autoCenter: Phaser.Scale.CENTER_BOTH, - width: 1280, - height: 720, - min: { width: 640, height: 360 }, - max: { width: 1920, height: 1080 } - }, - backgroundColor: '#2d2d2d', - scene: MyScene -}; -``` - -Custom FPS limit: - -```js -const config = { - type: Phaser.AUTO, - width: 800, - height: 600, - fps: { target: 60, limit: 30, forceSetTimeOut: false, smoothStep: true }, - scene: MyScene -}; -``` - -Transparent canvas over HTML — when `transparent` is `true`, `backgroundColor` is forced to `0x000000` with alpha `0`: - -```js -const config = { - type: Phaser.AUTO, - width: 800, - height: 600, - transparent: true, - parent: 'game-container', - scene: MyScene -}; -``` - -Pre-existing canvas element: - -```js -const canvas = document.getElementById('my-canvas'); -const config = { - type: Phaser.WEBGL, - canvas: canvas, - width: 800, - height: 600, - scene: MyScene -}; -``` - -Multiple scenes — only the first scene starts automatically; others start only if they have `{ active: true }` in their scene config: - -```js -const config = { - type: Phaser.AUTO, - width: 800, - height: 600, - scene: [BootScene, PreloadScene, MenuScene, GameScene], - physics: { default: 'arcade', arcade: { gravity: { y: 300 } } } -}; -``` - -Boot callbacks: - -```js -const config = { - type: Phaser.AUTO, - width: 800, - height: 600, - callbacks: { - preBoot: function (game) { /* before boot; game systems not yet available */ }, - postBoot: function (game) { /* after boot; all systems ready, loop starting */ } - }, - scene: MyScene -}; -``` - -Disabling input subsystems: - -```js -const config = { - type: Phaser.AUTO, - width: 800, - height: 600, - input: { - keyboard: true, mouse: true, touch: false, - gamepad: false, activePointers: 1, windowEvents: true - }, - disableContextMenu: true, - scene: MyScene -}; -``` - -**Scale modes** (`scale.mode`): `NONE` (0, no scaling), `WIDTH_CONTROLS_HEIGHT` (1), `HEIGHT_CONTROLS_WIDTH` (2), `FIT` (3, fit inside parent keeping aspect ratio — letterbox), `ENVELOP` (4, cover parent keeping aspect ratio — may overflow), `RESIZE` (5, fill parent ignoring aspect ratio), `EXPAND` (6, RESIZE for visible area + FIT for canvas scale). **Center modes** (`scale.autoCenter`): `NO_CENTER` (0), `CENTER_BOTH` (1), `CENTER_HORIZONTALLY` (2), `CENTER_VERTICALLY` (3). **Zoom constants** (`scale.zoom`): `NO_ZOOM` (1), `ZOOM_2X` (2), `ZOOM_4X` (4), `MAX_ZOOM` (-1, max integer zoom that fits in parent). -</common_patterns> - -<lifecycle_and_globals> -Listen on `game.events` (or `this.game.events` from a Scene) for visibility and focus changes (use named constants `Phaser.Core.Events.BLUR`, `FOCUS`, `HIDDEN`, `VISIBLE`, `PAUSE`, `RESUME`): - -```js -game.events.on('blur', () => { /* tab lost focus */ }); -game.events.on('focus', () => { /* tab regained focus */ }); -game.events.on('hidden', () => { /* Page Visibility API: page hidden */ }); -game.events.on('visible', () => { /* Page Visibility API: page visible */ }); -game.events.on('pause', () => { /* game.pause() was called */ }); -game.events.on('resume', () => { /* game.resume() was called */ }); -``` - -Pause, resume, and destroy: - -```js -game.pause(); game.resume(); -if (game.isPaused) { /* game loop is not running */ } - -game.destroy(true); // remove canvas from DOM and clean up -game.destroy(true, true); // noReturn=true: NEVER creating another instance -``` - -`destroy(removeCanvas, noReturn)` is asynchronous — it flags the game for destruction on the next frame. Listen for `Phaser.Core.Events.DESTROY` to react to completion. - -The `Game` instance exposes key managers as properties, accessible from any Scene via `this.game`: `game.config` (parsed Config, read-only after boot), `game.scene` (SceneManager — start/stop/switch scenes), `game.registry` (DataManager — shared runtime data manager across all scenes), `game.anims` (global animation definitions), `game.cache` (loaded non-texture assets), `game.textures` (loaded textures and sprite sheets), `game.sound` (global sound manager). - -```js -this.game.registry.set('highScore', 9999); // shared registry from any scene -this.game.scene.start('MenuScene'); // scene manager -``` -</lifecycle_and_globals> - -<gotchas> -- **scale vs top-level config**: `width`, `height`, `zoom`, and `parent` can be set at top level or inside `scale`; the `scale` sub-object wins. Avoid setting both. -- **Phaser.WEBGL has no fallback**: unlike `AUTO`, it will not fall back to Canvas if WebGL is unavailable — the game fails silently. -- **parent: null vs parent: undefined**: `undefined` (or omitted) appends the canvas to `document.body`. `null` means Phaser does not add the canvas to the DOM at all — you must do it yourself. -- **transparent overrides backgroundColor**: when `transparent: true`, the background is forced to `rgba(0,0,0,0)` regardless of what you set. -- **fps.target does not enforce frame rate**: it is advisory only. Use `fps.limit` to actually cap. The browser's display refresh rate is always the upper bound. -- **fps.limit only slows down, never speeds up**: `limit: 120` on a 60Hz display still results in 60 FPS. -- **DOM Container requires a parent**: `dom.createContainer: true` without a `parent` element will not work. -- **smoothPixelArt is WebGL-only**: no effect with the Canvas renderer. -- **window.FORCE_WEBGL / window.FORCE_CANVAS**: Config.js checks these globals at the end of parsing and overrides the `type` setting (intended for development/testing). -- **Game.destroy() is asynchronous**: it flags destruction on the next frame; listen for the `DESTROY` event. Pass `noReturn: true` only if you will never create another Phaser instance on the same page. -</gotchas> -</game_setup_and_config> diff --git a/skills/base44-platform-games/references/phaser/geometry_and_math.md b/skills/base44-platform-games/references/phaser/geometry_and_math.md deleted file mode 100644 index 0fd3d6c..0000000 --- a/skills/base44-platform-games/references/phaser/geometry_and_math.md +++ /dev/null @@ -1,156 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: geometry and math - -<geometry_and_math> -Use this when using Phaser 4 math and geometry utilities: Geom classes (Circle, Ellipse, Line, Polygon, Rectangle, Triangle), intersection tests, vectors and matrices, angles, distance, interpolation, easing, random number generation, snapping, clamping, and color utilities. - -<quick_start> -```js -// Geometry objects are data only — not renderable -const rect = new Phaser.Geom.Rectangle(10, 20, 200, 100); -const circle = new Phaser.Geom.Circle(400, 300, 50); -const line = new Phaser.Geom.Line(0, 0, 100, 100); - -rect.contains(50, 50); // true — point containment -circle.contains(410, 310); // true -Phaser.Geom.Intersects.CircleToRectangle(circle, rect); // boolean - -// Math utilities -const v = new Phaser.Math.Vector2(3, 4); -v.length(); // 5 -v.normalize(); // {x: 0.6, y: 0.8} - -const dist = Phaser.Math.Distance.Between(0, 0, 100, 100); -const angle = Phaser.Math.Angle.Between(0, 0, 100, 100); -const clamped = Phaser.Math.Clamp(150, 0, 100); // 100 -``` -</quick_start> - -<core_concepts> -Geometry objects are pure data containers holding coordinates and dimensions. They are NOT game objects and cannot be added to the display list — to render geometry, use the Graphics game object or Shape game objects. Every geom class has a `type` property set to a `Phaser.Geom` constant for fast type checks. - -All geom classes share a common instance method pattern: `contains(x, y)` (point-in-shape), `getPoint(position, output?)` (point at normalized 0-1 position on perimeter), `getPoints(quantity, stepRate?, output?)` (evenly spaced perimeter points), `getRandomPoint(output?)` (random point inside), `setTo(...)`, `setEmpty()`, `setPosition(x, y)`. Each geom type also has a folder of static helper functions (e.g. `Phaser.Geom.Rectangle.Contains`, `Phaser.Geom.Circle.Area`). -</core_concepts> - -<geometry_classes> -Constructors and key properties: -- **Circle** `Phaser.Geom.Circle(x, y, radius)` — `x`, `y`, `radius`, `diameter`, `left`, `right`, `top`, `bottom`. -- **Ellipse** `Phaser.Geom.Ellipse(x, y, width, height)` — `x`, `y`, `width`, `height`, edges. -- **Line** `Phaser.Geom.Line(x1, y1, x2, y2)` — `x1`, `y1`, `x2`, `y2`, edges. -- **Polygon** `Phaser.Geom.Polygon(points)` — `area`, `points` (array of Vector2Like). -- **Rectangle** `Phaser.Geom.Rectangle(x, y, width, height)` — `x`, `y`, `width`, `height`, edges, `centerX`, `centerY`. -- **Triangle** `Phaser.Geom.Triangle(x1, y1, x2, y2, x3, y3)` — three vertex pairs. - -The Polygon constructor accepts multiple `points` formats: space-separated string (`'40 0 40 20 100 20'`), array of `{x, y}` objects, flat number array (`[x1, y1, x2, y2, ...]`), or array of `[x, y]` sub-arrays. - -Each type has a rich set of static helpers under `Phaser.Geom.<Type>.*` — e.g. Rectangle: Area, CenterOn, Contains, ContainsRect, FitInside, FitOutside, Inflate, Intersection, MarchingAnts, Offset, Overlaps, Perimeter, Random, Scale, Union; Circle: Area, Circumference, CircumferencePoint, Contains, GetBounds, GetPoint, Random; Line: Angle, BresenhamPoints, Extend, GetMidPoint, GetNearestPoint, GetNormal, Length, Rotate, SetToAngle, Slope; Triangle: Area, BuildEquilateral, BuildRight, Centroid, CircumCenter, CircumCircle, InCenter, Rotate. -</geometry_classes> - -<intersection_tests> -All under `Phaser.Geom.Intersects`. - -Boolean tests (return `true`/`false`): `CircleToCircle(a, b)`, `CircleToRectangle(circle, rect)`, `LineToCircle(line, circle)`, `LineToLine(l1, l2, out?)` (writes point to `out`), `LineToRectangle(line, rect)`, `PointToLine(point, line, lineThickness?)`, `PointToLineSegment(point, line)`, `RectangleToRectangle(a, b)`, `RectangleToTriangle(rect, tri)`, `RectangleToValues(rect, left, right, top, bottom, tolerance?)`, `TriangleToCircle(tri, circle)`, `TriangleToLine(tri, line)`, `TriangleToTriangle(a, b)`. - -Get intersection points (return `Vector2[]`, accept an optional `out` array): `GetCircleToCircle`, `GetCircleToRectangle`, `GetLineToCircle`, `GetLineToLine` (single point), `GetLineToPoints`, `GetLineToPolygon` (closest intersection), `GetLineToRectangle`, `GetRaysFromPointToPolygon(x, y, polygon)`, `GetRectangleIntersection` (overlapping region), `GetRectangleToRectangle`, `GetRectangleToTriangle`, `GetTriangleToCircle`, `GetTriangleToLine`, `GetTriangleToTriangle`. -</intersection_tests> - -<math_functions> -All under `Phaser.Math` unless noted. - -**Constants**: `TAU` (PI * 2, v4 addition), `PI_OVER_2`, `EPSILON` (1.0e-6), `DEG_TO_RAD` (PI / 180), `RAD_TO_DEG` (180 / PI), `RND` (global `RandomDataGenerator`, seeded via game config `seed`). - -**Angle** (`Phaser.Math.Angle.*`): `Between(x1, y1, x2, y2)`, `BetweenPoints(p1, p2)`, `BetweenY` / `BetweenPointsY` (from vertical axis), `CounterClockwise(angle)`, `GetClockwiseDistance(from, to)`, `GetCounterClockwiseDistance`, `GetShortestDistance` (signed), `Normalize(angle)` (to [0, 2PI)), `Random()`, `RandomDegrees()`, `Reverse(angle)` (add PI), `RotateTo(current, target, lerp?)`, `ShortestBetween(a1, a2)` (degrees), `Wrap(angle)` (to (-PI, PI]), `WrapDegrees(angle)` (to (-180, 180]). - -**Distance** (`Phaser.Math.Distance.*`): `Between(x1, y1, x2, y2)` (Euclidean), `BetweenPoints(a, b)`, `BetweenPointsSquared`, `Chebyshev` (chessboard), `Power(x1, y1, x2, y2, pow)` (Minkowski), `Snake` (Manhattan/taxicab), `Squared` (squared Euclidean — avoids sqrt). - -**Interpolation** (`Phaser.Math.Interpolation.*`): `BezierInterpolation(v, k)`, `CatmullRomInterpolation(v, k)`, `CubicBezierInterpolation(t, p0, p1, p2, p3)`, `LinearInterpolation(v, k)`, `QuadraticBezierInterpolation(t, p0, p1, p2)`, `SmoothStepInterpolation(t, min, max)`, `SmootherStepInterpolation(t, min, max)`. - -**Snap** (`Phaser.Math.Snap.*`): `To(value, gap, start?, divide?)` (nearest increment), `Floor(...)` (down), `Ceil(...)` (up). - -**Fuzzy** (`Phaser.Math.Fuzzy.*`): `Equal(a, b, epsilon?)`, `LessThan`, `GreaterThan`, `Ceil(value, epsilon?)`, `Floor(value, epsilon?)`. - -**Easing** (`Phaser.Math.Easing.*`, used primarily by tweens via string names like `'Sine.easeOut'`): each type has `.In`, `.Out`, `.InOut` variants — Back, Bounce, Circular (`Circ`), Cubic, Elastic, Expo, Quadratic (`Quad`), Quartic (`Quart`), Quintic (`Quint`), Sine. No-variant types: `Linear`, `Stepped`. Power aliases: `Power0` = Linear, `Power1` = Quad.Out, `Power2` = Cubic.Out, `Power3` = Quart.Out, `Power4` = Quint.Out. Short names default to `.Out` (`'Quad'` = Quad.Out). - -**Core helpers** (directly on `Phaser.Math`): `Between(min, max)` (random int), `FloatBetween(min, max)` (random float), `Clamp(value, min, max)`, `Wrap(value, min, max)`, `Within(a, b, tolerance)`, `Percent(value, min, max, upperMax?)`, `FromPercent(percent, min, max)`, `DegToRad(degrees)`, `RadToDeg(radians)`, `Linear(p0, p1, t)` (lerp), `LinearXY(v1, v2, t, out?)`, `SmoothStep(x, min, max)`, `SmootherStep(x, min, max)`, `Average(values)`, `Median(values)`, `CeilTo(value, place?, base?)`, `FloorTo`, `RoundTo`, `RoundAwayFromZero(value)`, `MaxAdd(value, amount, max)`, `MinSub(value, amount, min)`, `Difference(a, b)`, `IsEven(value)`, `IsEvenStrict(value)`, `Factorial(value)`, `Rotate(point, angle)`, `RotateAround(point, cx, cy, angle)`, `RotateAroundDistance(point, cx, cy, angle, dist)`, `TransformXY(x, y, posX, posY, rotation, scaleX, scaleY, output?)`, `GetSpeed(distance, time)`, `RandomXY(vector, scale?)`. - -**Power-of-Two** (`Phaser.Math.Pow2.*`): `GetPowerOfTwo(value)`, `IsValuePowerOfTwo(value)`, `IsSizePowerOfTwo(width, height)`. -</math_functions> - -<vectors_and_matrices> -**Vector2** (`Phaser.Math.Vector2`) — 2D vector for positions, velocities, directions. Constructor: `new Vector2(x?, y?)` or `new Vector2({x, y})`; if only `x` given, `y` defaults to `x`. Methods mutate in place and return `this` for chaining: `set`/`setTo(x, y)`, `setToPolar(angle, length?)`, `copy(src)` / `setFromObject(obj)`, `clone()`, `add`/`subtract(src)`, `multiply`/`divide(src)`, `scale(value)`, `negate()`, `normalize()`, `normalizeRightHand()` / `normalizeLeftHand()` (perpendiculars), `limit(max)`, `setLength(length)`, `length()`, `lengthSq()`, `distance(src)`, `distanceSq(src)`, `dot(src)`, `cross(src)` (2D scalar), `angle()` (radians from +x axis), `setAngle(angle)`, `rotate(delta)`, `lerp(src, t)`, `reflect(normal)`, `mirror(axis)`, `project(src)`, `equals(v)`, `fuzzyEquals(v, epsilon?)`, `ceil()` / `floor()` / `invert()`, `reset()`, `transformMat3(mat)`, `transformMat4(mat)`. Static constants: `Vector2.ZERO`, `RIGHT`, `LEFT`, `UP`, `DOWN`, `ONE`. - -**Vector3** (`Phaser.Math.Vector3`) — for camera projections, lighting, 3D math. Same patterns as Vector2 plus: `up()`, `min(v)`, `max(v)`, `addVectors(a, b)`, `subVectors(a, b)`, `crossVectors(a, b)`, `cross(v)`, `addScalar(s)`, `addScale(v, scale)`, `fromArray(array, offset?)`, `setFromMatrixPosition(mat4)`, `setFromMatrixColumn(mat4, index)`, `applyMatrix3(mat)`, `applyMatrix4(mat)`, `transformCoordinates(mat)`, `transformQuat(q)`, `project(mat)`, `projectViewMatrix(view, proj)`, `unproject(viewport, invProjView)`. - -**Matrix4** (`Phaser.Math.Matrix4`) — 4x4 matrix for 3D transforms, projection, and view matrices, backed by `Float32Array(16)`. Constructor `new Matrix4(m?)` copies an existing Matrix4 or defaults to identity. Key methods: `clone()`, `set`/`copy(src)`, `identity()`, `transpose()`, `invert()`, `adjoint()`, `determinant()`, `multiply`/`multiplyLocal(src)`, `translate(v)`, `scale(v)`, `rotate(angle, axis)`, `rotateX`/`rotateY`/`rotateZ(angle)`, `fromRotationTranslation(q, v)`, `fromQuat(q)`, `frustum(...)`, `perspective(fovy, aspect, near, far)`, `perspectiveLH(width, height, near, far)`, `ortho(left, right, bottom, top, near, far)`, `lookAt(eye, center, up)`, `lookAtRH(eye, target, up)`, `setWorldMatrix(rotation, position, scale, viewMatrix?, projectionMatrix?)`. Also `Phaser.Math.Matrix3` — 3x3 matrix for 2D transforms and normal matrices. -</vectors_and_matrices> - -<random_data_generator> -`Phaser.Math.RandomDataGenerator` — seeded PRNG. Global instance at `Phaser.Math.RND` (seeded via game config `seed`). Provide seeds for reproducible sequences: `new RandomDataGenerator(['level-42'])`. - -```js -const rnd = Phaser.Math.RND; - -rnd.integer(); // random integer in [0, 2^32] -rnd.frac(); // random float in [0, 1) -rnd.between(1, 10); // random integer in [1, 10] -rnd.integerInRange(1, 10); // alias for between -rnd.realInRange(0.5, 1.5); // random float in [0.5, 1.5] -rnd.normal(); // normal distribution around 0 -rnd.angle(); // random angle in radians (-PI to PI) -rnd.rotation(); // random rotation in radians (same range) -rnd.pick(array); // random element -rnd.weightedPick(array); // weighted toward end of array -rnd.sign(); // -1 or 1 -rnd.uuid(); // RFC4122 v4 UUID string -rnd.shuffle(array); // in-place Fisher-Yates shuffle -rnd.state(state?); // get/set deterministic RNG state string -``` -</random_data_generator> - -<color_utilities> -**Color class** (`Phaser.Display.Color`) — mutable RGBA color with automatic conversion to WebGL floats, HSV, CSS strings, and packed integers. - -```js -const color = new Phaser.Display.Color(255, 0, 0, 255); // RGBA (0-255) - -// Creation helpers (return Color instances) -const c1 = Phaser.Display.Color.ValueToColor('#ff0000'); // hex string, integer, or object -const c2 = Phaser.Display.Color.HexStringToColor('#ff0000'); -const c3 = Phaser.Display.Color.RGBStringToColor('rgb(255,0,0)'); -const c4 = Phaser.Display.Color.HSVToRGB(0.5, 1, 1); // returns { r, g, b, color } - -// Properties -color.r; color.g; color.b; color.a; // 0-255 integers -color.redGL; color.greenGL; color.blueGL; color.alphaGL; // 0-1 floats (WebGL) -color.color; // packed 24-bit integer (0xRRGGBB) -color.color32; // packed 32-bit integer (0xAARRGGBB) -color.rgba; // CSS string 'rgba(r,g,b,a)' -color.h; color.s; color.v; // HSV (read-only, auto-updated) -``` - -Manipulation (amount typically 0-100, returns the Color): `brighten(25)`, `saturate(50)`, `desaturate(30)`, `lighten(20)`, `darken(10)`, `random()` (optional min/max range), `gray(128)`. Setting values: `setTo(255, 128, 0, 255)`, `setFromRGB({ r, g, b, a })`, `setFromHSV(0.1, 0.8, 1.0)` (HSV 0-1). - -Interpolation (`Phaser.Display.Color.Interpolate`, returns `{ r, g, b, a, color }`): - -```js -const result = Phaser.Display.Color.Interpolate.RGBWithRGB(255, 0, 0, 0, 0, 255, 100, 50); // midpoint purple -const mid = Phaser.Display.Color.Interpolate.ColorWithColor(color1, color2, 100, 50); -const hsvResult = Phaser.Display.Color.Interpolate.HSVWithHSV(0, 1, 1, 0.5, 1, 1, 100, 50); // v4 smooth hue -``` -</color_utilities> - -<gotchas> -- **Geom objects are not game objects**: no `scene`, no display-list `setPosition`, no texture. Use Graphics or Shape game objects to render them. -- **Point class removed in v4**: use `Phaser.Math.Vector2` or plain `{x, y}` objects. The `GEOM_CONST.POINT` (3) exists but the Point class does not. -- **Angles are in radians** throughout the math API. Use `DegToRad()` / `RadToDeg()` to convert. `Phaser.Math.TAU` (2 * PI) is new in v4. -- **Vector2 methods mutate in place** and return `this`. Call `.clone()` first to preserve the original: `const result = v.clone().add(other)`. -- **Squared distance is faster** than Euclidean (avoids `Math.sqrt`). Use `Distance.Squared` or `vec.distanceSq()` for comparisons where the actual distance value isn't needed. -- **Intersection Get* functions allocate arrays**: pass a reusable `out` array to avoid GC pressure in hot loops. -- **EaseMap short names default to `.Out`**: `'Quad'` means `Quadratic.Out`, not `.In`. Use full keys like `'Quad.easeIn'` for other variants. -- **RandomDataGenerator state is serializable**: `rnd.state()` returns a string; if it must survive reloads, persist that string only as a field in a Base44 entity. -- **Polygon.contains uses ray-casting** (even/odd rule). Complex self-intersecting polygons may give unexpected results. -- **Matrix4.val is a Float32Array**: access elements via `mat.val[index]` in column-major order (OpenGL convention). -</gotchas> -</geometry_and_math> diff --git a/skills/base44-platform-games/references/phaser/graphics_and_shapes.md b/skills/base44-platform-games/references/phaser/graphics_and_shapes.md deleted file mode 100644 index 8be07e5..0000000 --- a/skills/base44-platform-games/references/phaser/graphics_and_shapes.md +++ /dev/null @@ -1,229 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: graphics and shapes - -<graphics_and_shapes> -Use this when drawing shapes and graphics in Phaser 4: the Graphics game object (lines, rectangles, circles, arcs, polygons, gradients, fill, stroke, generated textures) and Shape game objects (Arc, Curve, Ellipse, Grid, IsoBox, IsoTriangle, Line, Polygon, Rectangle, Star, Triangle). - -<quick_start> -```js -// Draw a filled rectangle and stroked circle with Graphics -const gfx = this.add.graphics(); - -gfx.fillStyle(0x00aa00, 1); -gfx.fillRect(50, 50, 200, 100); - -gfx.lineStyle(3, 0xff0000, 1); -gfx.strokeCircle(400, 150, 60); -``` - -```js -// Same shapes as standalone Shape game objects -const rect = this.add.rectangle(150, 100, 200, 100, 0x00aa00); -const circle = this.add.circle(400, 150, 60); -circle.setStrokeStyle(3, 0xff0000); -``` -</quick_start> - -<core_concepts> -Phaser offers two approaches for rendering primitives without textures. - -**Graphics game object** — created with `this.add.graphics(config?)` where config is `{ x?, y?, lineStyle?, fillStyle? }`. An imperative drawing surface: you call methods like `fillRect`, `strokeCircle`, `beginPath`/`lineTo`/`strokePath` to build a command buffer that replays each frame. Supports paths, arcs, gradients, rounded rectangles, canvas transforms (`translateCanvas`, `scaleCanvas`, `rotateCanvas`), and `save`/`restore`. Can bake a Texture via `generateTexture(key, width, height)`. Expensive to render (its own WebGL shader) — group Graphics objects to minimize batch flushes. Components: AlphaSingle, BlendMode, Depth, Lighting, Mask, RenderNodes, Transform, Visible, ScrollFactor. Does NOT include Origin or GetBounds — position via options or `setPosition`. - -**Shape game objects** — individual objects (Arc, Rectangle, Star, etc.) extending the base `Shape` class, each rendering one predefined shape with precomputed path data. Created via dedicated factories (`this.add.rectangle(...)`, `this.add.circle(...)`, etc.). Fully featured: tweenable, scalable, can join groups/containers, enabled for input/physics. Style via `setFillStyle(color, alpha)` and `setStrokeStyle(lineWidth, color, alpha)`. Share the same WebGL batch as Graphics. Do NOT support gradients, path detail threshold, or canvas transforms. Components: AlphaSingle, BlendMode, Depth, GetBounds, Lighting, Mask, Origin, RenderNodes, ScrollFactor, Transform, Visible (include Origin and GetBounds, unlike Graphics). - -**When to use which**: use **Graphics** for dynamic drawing, complex paths, multiple shapes on one object, gradients, or generating textures. Use **Shape objects** for individual UI elements, simple indicators, physics-enabled shapes, or anything that benefits from game object features (origin, bounds, input). -</core_concepts> - -<common_patterns> -Fill and stroke styles (Graphics) — call style methods BEFORE the corresponding fill/stroke; they are not retroactive: - -```js -const gfx = this.add.graphics(); - -gfx.fillStyle(0xff0000, 1); // (color, alpha) — before fill* -gfx.lineStyle(4, 0x00ff00, 1); // (width, color, alpha) — before stroke* - -// Gradient fill (WebGL only) — 4 corner colors -gfx.fillGradientStyle( - 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, // tl, tr, bl, br colors - 1, 1, 1, 1 // tl, tr, bl, br alphas -); -gfx.fillRect(0, 0, 300, 200); - -gfx.lineGradientStyle(2, 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 1); // gradient line (WebGL only) -``` - -Drawing primitives (Graphics): - -```js -gfx.fillRect(x, y, width, height); gfx.strokeRect(x, y, width, height); -gfx.fillCircle(x, y, radius); gfx.strokeCircle(x, y, radius); -gfx.fillEllipse(x, y, width, height, smoothness); // smoothness defaults to 32 -gfx.strokeEllipse(x, y, width, height, smoothness); -gfx.fillTriangle(x0, y0, x1, y1, x2, y2); gfx.strokeTriangle(x0, y0, x1, y1, x2, y2); -gfx.fillPoint(x, y, size); // size defaults to 1 (draws a small square) -gfx.lineBetween(x1, y1, x2, y2); -``` - -Rounded rectangles — radius can be a number or a per-corner object; negative values create concave corners (default 20): - -```js -gfx.fillRoundedRect(50, 50, 300, 200, 16); -gfx.strokeRoundedRect(50, 50, 300, 200, 16); -gfx.fillRoundedRect(50, 50, 300, 200, { tl: 20, tr: 20, bl: 0, br: 0 }); // per-corner -gfx.fillRoundedRect(50, 50, 300, 200, { tl: -10, tr: -10, bl: -10, br: -10 }); // concave -``` - -Path drawing — Graphics `arc`/`slice` take angles in RADIANS: - -```js -gfx.beginPath(); -gfx.moveTo(100, 100); -gfx.lineTo(200, 50); -gfx.lineTo(300, 100); -gfx.lineTo(250, 200); -gfx.closePath(); -gfx.strokePath(); // or gfx.stroke() — alias - -gfx.fillStyle(0x00aaff); -gfx.beginPath(); -gfx.moveTo(100, 100); -gfx.lineTo(200, 50); -gfx.lineTo(300, 100); -gfx.closePath(); -gfx.fillPath(); // or gfx.fill() — alias - -// Arc within a path (x, y, radius, startAngle, endAngle, anticlockwise, overshoot) -gfx.beginPath(); -gfx.arc(200, 200, 80, 0, Math.PI / 2, false, 0); -gfx.strokePath(); - -// Pie slice — auto begins/closes path -gfx.slice(200, 200, 80, 0, Math.PI / 3, false); -gfx.fillPath(); - -// From point arrays -gfx.strokePoints(points, closeShape, closePath, endIndex); -gfx.fillPoints(points, closeShape, closePath, endIndex); -``` - -Geom shape helpers — Graphics accepts `Phaser.Geom` objects directly: `fillCircleShape(circle)`, `strokeCircleShape(circle)`, `fillRectShape(rect)`, `strokeRectShape(rect)`, `fillTriangleShape(tri)`, `strokeTriangleShape(tri)`, `strokeLineShape(line)`, `fillEllipseShape(ellipse, smoothness)`, `strokeEllipseShape(ellipse, smoothness)`. - -Canvas transforms (Graphics): - -```js -gfx.save(); -gfx.translateCanvas(100, 100); -gfx.rotateCanvas(0.5); // radians -gfx.scaleCanvas(2, 2); -gfx.fillStyle(0xff0000); -gfx.fillRect(0, 0, 50, 50); // drawn at translated/rotated/scaled position -gfx.restore(); -``` - -Generating textures from Graphics — gradient fills do NOT appear in generated textures (Canvas API limitation): - -```js -const gfx = this.add.graphics(); -gfx.fillStyle(0xff0000); -gfx.fillCircle(32, 32, 32); -gfx.generateTexture('redCircle', 64, 64); -gfx.destroy(); - -this.add.image(400, 300, 'redCircle'); // now a regular texture -``` - -Shape objects — fill and stroke: - -```js -const rect = this.add.rectangle(200, 150, 100, 80, 0xff0000, 1); - -rect.setFillStyle(0x00ff00, 0.8); // change fill later -rect.setStrokeStyle(3, 0xffffff, 1); // add stroke (not set by default) -rect.setFillStyle(); // no args = isFilled becomes false -rect.setStrokeStyle(); // no args = isStroked becomes false - -// Direct property access -rect.fillColor = 0x0000ff; rect.fillAlpha = 0.5; -rect.strokeColor = 0xffffff; rect.strokeAlpha = 1; rect.lineWidth = 2; -rect.isFilled = true; rect.isStroked = true; -rect.closePath = true; // close stroke path (default true) -``` -</common_patterns> - -<shape_types> -Factory methods (all after `x, y`); defaults in parentheses: -- `this.add.arc(x, y, radius, startAngle, endAngle, anticlockwise, fillColor, fillAlpha)` — class Arc. radius=128, startAngle=0, endAngle=360 (DEGREES), anticlockwise=false. Fill + stroke. -- `this.add.circle(x, y, radius, fillColor, fillAlpha)` — Arc with 0-360 angles. radius=128. Fill + stroke. -- `this.add.curve(x, y, curve, fillColor, fillAlpha)` — Curve, takes a `Phaser.Curves.Curve`. Has `smoothness` / `setSmoothness()`. Fill + stroke. -- `this.add.ellipse(x, y, width, height, fillColor, fillAlpha)` — width=128, height=128 (equal renders as circle). Has `smoothness`. Fill + stroke. -- `this.add.grid(x, y, width, height, cellWidth, cellHeight, fillColor, fillAlpha, outlineFillColor, outlineFillAlpha)` — width/height=128, cellWidth/cellHeight=32. `altFillColor`/`altFillAlpha` for checkerboard; outline via outlineFillColor. Fill only. -- `this.add.isobox(x, y, size, height, fillTop, fillLeft, fillRight)` — size=48, height=32, fillTop=0xeeeeee, fillLeft=0x999999, fillRight=0xcccccc. `showTop`/`showLeft`/`showRight`/`projection`. Fill only. -- `this.add.isotriangle(x, y, size, height, reversed, fillTop, fillLeft, fillRight)` — isometric pyramid. size=48, height=32, reversed=false. Fill only. -- `this.add.line(x, y, x1, y1, x2, y2, strokeColor, strokeAlpha)` — x1=0, y1=0, x2=128, y2=0. STROKE ONLY; constructor takes stroke color (not fill). -- `this.add.polygon(x, y, points, fillColor, fillAlpha)` — points as array of Vec2, `[x,y,...]` pairs, or `[[x,y],...]`. Fill + stroke. -- `this.add.rectangle(x, y, width, height, fillColor, fillAlpha)` — width/height=128, change via `width`/`height`. Fill + stroke. -- `this.add.star(x, y, points, innerRadius, outerRadius, fillColor, fillAlpha)` — points=5, innerRadius=32, outerRadius=64 (4 points = diamond, more = spikier). Fill + stroke. -- `this.add.triangle(x, y, x1, y1, x2, y2, x3, y3, fillColor, fillAlpha)` — x1=0,y1=128, x2=64,y2=0, x3=128,y3=128. Always closed (use Polygon for open shapes). Fill + stroke. -</shape_types> - -<shape_specific_methods> -Star: - -```js -const star = this.add.star(400, 300, 5, 32, 64, 0xffff00); -star.setPoints(8); // change number of points -star.setInnerRadius(20); -star.setOuterRadius(80); -``` - -Line (tapering — `endWidth` is WebGL only; Canvas uses startWidth only): - -```js -const line = this.add.line(400, 300, 0, 0, 200, 100, 0xffffff); -line.setLineWidth(4, 1); // tapers from 4px to 1px -``` - -Grid (alternating colors, stroke, padding): - -```js -const grid = this.add.grid(400, 300, 320, 240, 32, 32, 0x222222); -grid.setAltFillStyle(0x444444, 1); // checkerboard; no args = disable alternating cells -grid.setStrokeStyle(1, 0x666666, 1); // cell outlines (inherited from Shape) -grid.strokeOutside = true; // v4: stroke the outer border -grid.strokeOutsideIncomplete = false; // skip partial cells on right/bottom edges -grid.cellPadding = 1; // gutter between cells is 2x this value (default 0.5) -``` - -IsoBox / IsoTriangle: - -```js -const box = this.add.isobox(200, 200, 48, 32, 0xeeeeee, 0x999999, 0xcccccc); -box.setFaces(true, true, false); // showTop, showLeft, showRight -box.setProjection(4); - -const tri = this.add.isotriangle(400, 200, 48, 32, false, 0xeeeeee, 0x999999, 0xcccccc); -tri.setReversed(true); // flip upside down -tri.setFaces(true, true, true); -tri.setProjection(4); -``` -</shape_specific_methods> - -<gotchas> -- **Graphics arc() uses radians; the Shape arc factory uses degrees.** Graphics `arc` takes start/end angles in radians; `this.add.arc()` takes them in degrees (0-360). Mixing these up is the most common bug. -- **Set style BEFORE drawing.** `fillStyle`/`lineStyle` must be called before the corresponding fill/stroke method — they are not retroactive. -- **Graphics has no Origin or GetBounds.** Use `setPosition(x, y)` and `displayOriginX`/`displayOriginY` instead. -- **Shape isFilled/isStroked defaults.** A shape created with a `fillColor` parameter has `isFilled = true`, but `isStroked` defaults to `false` — call `setStrokeStyle()` explicitly. -- **Line shape is stroke-only.** Its constructor takes `strokeColor`/`strokeAlpha`, not fillColor. -- **IsoBox/IsoTriangle are fill-only.** They cannot be stroked. Grid supports outline strokes via constructor parameters (`outlineFillColor`, `outlineFillAlpha`). -- **generateTexture uses the Canvas API.** Gradient fills (`fillGradientStyle`) will not appear in generated textures — only Canvas-compatible features are captured. -- **Graphics is expensive.** Each frame the command buffer replays and geometry is rebuilt (WebGL decomposes to polygons). For static shapes, `generateTexture` and use the result as a Sprite. Group Graphics objects to minimize batch breaks. -- **pathDetailThreshold (v4 new).** Graphics has a `pathDetailThreshold` property (default -1, uses config `render.pathDetailThreshold`). Path segments below this pixel threshold are combined, improving WebGL performance on complex shapes. Evaluated in screen pixels, so detail emerges when zoomed in. -- **Rounded rect radius can be an object or number.** Pass `{ tl, tr, bl, br }` for per-corner control; negative values create concave corners; default is 20. -- **Shape closePath property** (default `true`) controls whether the stroke path is auto-closed. Set `false` for open stroked shapes. -- **Shape objects do NOT support tint.** Unlike Sprites/Images, they have no `setTint()` or `tint`. Use `setFillStyle(color, alpha)` and `setStrokeStyle(lineWidth, color, alpha)`. -- **Polygon getBounds() is incorrect with negative points.** If any polygon points have negative coordinates, `getBounds()` returns wrong values. Use `Phaser.Geom.Polygon.GetAABB(polygon.geom)` instead and adjust the returned Rectangle position. -</gotchas> -</graphics_and_shapes> diff --git a/skills/base44-platform-games/references/phaser/groups_and_containers.md b/skills/base44-platform-games/references/phaser/groups_and_containers.md deleted file mode 100644 index 8c447a4..0000000 --- a/skills/base44-platform-games/references/phaser/groups_and_containers.md +++ /dev/null @@ -1,221 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: groups and containers - -<groups_and_containers> -Use this when organizing game objects in Phaser 4: Groups for logical collections and object pooling, Containers for visual grouping with inherited transforms, Layers for render-order bucketing, plus batch operations and nested transforms. - -<quick_start> -```js -// In a Scene's create() method: - -// --- Group: logical collection, no transform, great for pooling --- -const enemies = this.add.group(); -enemies.create(100, 200, 'enemy'); // creates Sprite at (100,200) -enemies.create(300, 200, 'enemy'); - -// --- Container: visual parent with inherited transform --- -const hud = this.add.container(10, 10); -const icon = this.add.image(0, 0, 'heart'); -const label = this.add.text(20, 0, 'x3'); -hud.add([icon, label]); // children move/scale/rotate with hud - -// --- Layer: render-ordering bucket, no position/scale --- -const bgLayer = this.add.layer(); -const fgLayer = this.add.layer(); -bgLayer.add(this.add.image(400, 300, 'sky')); -fgLayer.add(this.add.sprite(400, 300, 'player')); -``` -</quick_start> - -<core_concepts> -Three ways to organize objects, each with a different purpose: - -**Group** (`this.add.group()`): a logical collection / pool. NOT on the display list — its children render individually on the Scene. No shared position, rotation, scale, or alpha. Members can belong to many Groups at once. Supports object pooling (`getFirstDead`, `kill`, `killAndHide`). Use for collections of similar objects (enemies, bullets, coins) and physics-group collisions. Children stored in a `children` Set (unordered, no index access). - -**Container** (`this.add.container(x, y)`): a visual parent on the display list that renders its children, which inherit its position, rotation, scale, and alpha. Exclusive by default — adding a child removes it from its previous Container (use `setExclusive(false)` to override). Use for composite UI (health bars, inventory slots), moving/rotating clusters as one unit, and nested transforms. Children stored in a `list` Array. Each child costs matrix math per frame, and deep nesting multiplies that cost — prefer Group or Layer when transforms aren't needed. - -**Layer** (`this.add.layer()`): a lightweight render-order bucket on the display list. No position, rotation, scale, scroll factor, input, or physics. Applies shared alpha/blend/mask. Children render in layer order, then by depth within the layer. Cannot be added to a Container (but Containers can be added to Layers). - -At a glance: if children must move/rotate together, use Container. If you need pooling or multi-membership, use Group. If you only need render ordering and shared effects, use Layer. -</core_concepts> - -<common_patterns> -Creating and populating Groups: -```js -// Empty group, add existing objects -const gems = this.add.group(); -gems.add(existingSprite); -gems.addMultiple([sprite1, sprite2, sprite3]); - -// Group with config -- creates children automatically -const coins = this.add.group({ - classType: Phaser.GameObjects.Sprite, - key: 'coin', - quantity: 10, // overrides frameQuantity - setXY: { x: 50, y: 300, stepX: 60 }, - setScale: { x: 0.5, y: 0.5 } -}); - -// Custom class type with pool limit -const bullets = this.add.group({ - classType: Bullet, // must accept (scene, x, y, key, frame) - maxSize: 30, - defaultKey: 'bullet', - runChildUpdate: true // calls child.update() each frame -}); -``` - -Object pooling with `get` / `getFirstDead` — deactivate objects instead of destroying them, then reuse inactive ones: -```js -const bullets = this.add.group({ - classType: Phaser.GameObjects.Sprite, - defaultKey: 'bullet', - maxSize: 30 -}); - -// Fire a bullet -- get() finds first inactive member or creates one -function fireBullet(x, y) { - const bullet = bullets.get(x, y); - if (bullet) { - bullet.setActive(true); - bullet.setVisible(true); - bullet.body.velocity.y = -300; // if physics enabled - } -} - -// Deactivate when off-screen or on hit -function killBullet(bullet) { - bullets.killAndHide(bullet); // sets active=false, visible=false - // If using physics, also reset the body: bullet.body.stop(); -} - -// Manual queries -const inactive = bullets.getFirst(false); // first where active===false -const active = bullets.getFirstAlive(); // first where active===true -const dead = bullets.getFirstDead(true, x, y); // first inactive, create if null -``` -Pool methods: `get(x, y, key, frame)` (shortcut for `getFirst(false, true, ...)`), `getFirstAlive`, `getFirstDead`, `getLast` (searches back-to-front), `kill` (active=false), `killAndHide` (active=false + visible=false), `countActive`, `getTotalUsed`, `getTotalFree` (remaining capacity), `isFull`. - -Physics Groups extend Group with automatic body assignment: -```js -// Arcade Physics group -- every member gets a dynamic body -const enemies = this.physics.add.group({ - key: 'enemy', quantity: 5, setXY: { x: 100, y: 100, stepX: 80 } -}); - -// Static physics group -- immovable bodies -const platforms = this.physics.add.staticGroup(); -platforms.create(400, 568, 'ground'); - -this.physics.add.collider(player, platforms); -this.physics.add.overlap(bullets, enemies, onHit); -``` - -Containers with nested transforms: -```js -// HUD that follows camera -const hud = this.add.container(10, 10); -hud.setScrollFactor(0); // pinned to camera -const healthBar = this.add.rectangle(0, 0, 200, 20, 0x00ff00); -const healthText = this.add.text(210, -5, '100 HP'); -hud.add([healthBar, healthText]); - -hud.setPosition(50, 50); // move everything at once -hud.setScale(1.5); // scale propagates to children -hud.setRotation(0.1); // rotation propagates to children -hud.setAlpha(0.8); // alpha affects all children - -// Nested containers -const inventory = this.add.container(300, 500); -for (let i = 0; i < 5; i++) { - const slot = this.add.container(i * 55, 0); - slot.add([ - this.add.rectangle(0, 0, 48, 48, 0x333333), - this.add.image(0, 0, `item-${i}`) - ]); - inventory.add(slot); // Container inside Container -} -``` -Key Container methods: `add` / `addAt` (removes child from display list), `remove(child, destroyChild)`, `getAt` / `getIndex`, `getByName` / `getFirst(prop, val)`, `getAll` / `count`, ordering (`sort`, `swap`, `moveTo`, `moveUp`/`moveDown`, `sendToBack`/`bringToTop`), `each` / `iterate` (iterate passes index), `setScrollFactor(x, y, updateChildren)` (pass `true` to apply to children), `getBounds`, `pointToContainer` (world → local), `setExclusive`, `replace`, `setSize(w, h)` (required before `setInteractive`), `length` (read-only count). - -Layers for render ordering: -```js -const bgLayer = this.add.layer(); -const entityLayer = this.add.layer(); -const uiLayer = this.add.layer(); - -bgLayer.add(this.add.image(400, 300, 'sky')); -entityLayer.add(player); -entityLayer.add(enemy); -uiLayer.add(scoreText); - -bgLayer.setDepth(0); // depth of layers themselves -entityLayer.setDepth(1); -uiLayer.setDepth(2); -enemy.setDepth(5); // depth within entityLayer, not the Scene - -// Shared effects to the entire layer -entityLayer.setAlpha(0.5); -entityLayer.setVisible(false); -entityLayer.setBlendMode(Phaser.BlendModes.ADD); -``` - -Bulk creation with `createMultiple` (accepts a config or array of configs): -```js -const coins = this.add.group(); -coins.createMultiple({ - key: 'coin', - quantity: 20, - setXY: { x: 50, y: 100, stepX: 40, stepY: 0 }, - setScale: { x: 0.5, y: 0.5 }, - setRotation: { value: 0, step: 0.1 }, // each rotated 0.1 more than previous - setAlpha: { value: 1 }, - setOrigin: { x: 0.5, y: 0.5 }, - setDepth: { value: 5 }, - gridAlign: { width: 5, height: 4, cellWidth: 48, cellHeight: 48, x: 100, y: 200 } -}); -``` - -Iterating and batch operations on Groups. Many setters support a step argument that distributes incremental values across members: -```js -const enemies = this.add.group(); -const all = enemies.getChildren(); // array snapshot -const active = enemies.getMatching('active', true); - -enemies.setXY(200, 300); -enemies.incX(5); // add 5 to each member's x -enemies.setVisible(false); -enemies.propertyValueSet('tintTopLeft', 0xff0000); -enemies.playAnimation('walk'); - -// Stepping: first member gets base value, each subsequent adds the step -enemies.setX(100, 50); // x = 100, 150, 200... -enemies.setXY(100, 200, 50, 30); // combined X and Y stepping -enemies.angle(0, 15); // 0, 15, 30 deg... -enemies.setAlpha(1, -0.1); // 1.0, 0.9, 0.8... -enemies.setScale(1, 0, 0.1, 0); // scaleX: 1.0, 1.1, 1.2... -enemies.setDepth(0, 1); // depth 0, 1, 2, 3... -enemies.setTint(0xff0000); -enemies.shuffle(); // randomize order -``` -</common_patterns> - -<gotchas> -- Group is NOT on the display list. Its children appear on the Scene individually. Moving a Group does nothing visually — use a Container for that. -- Container has performance overhead — every child requires extra matrix math per frame, multiplied by nesting depth. Avoid Containers when a Group or Layer suffices. -- Container origin is always 0,0 and cannot be changed. Position children relative to (0,0). -- Container children lose Scene-level depth control. A child's `depth` only orders within the Container; the Container's own depth positions it in the Scene. -- Physics + Container is problematic. If a Container is not at (0,0), physics bodies on children are offset. Avoid physics bodies on Container children. -- Container children cannot be individually masked in Canvas rendering — only the Container itself can have a mask, and masks don't stack for nested Containers. Masks DO stack in WebGL. -- `Group.get(x, y)` is shorthand for `getFirst(false, true, x, y)` — finds first inactive member and creates if none. `getFirst(state)` defaults to `active===false` without auto-creating. -- Layer cannot go inside a Container (only the reverse). -- Group children Set is unordered — no index access; use `getChildren()` for an array snapshot. -- Phaser 4 removed `children.each` / `children.iterate` from Group — Container still has them, Group does not. `children.forEach` replaces them, but `return false` no longer breaks out of it. -- `killAndHide` does NOT remove from the group — it only sets `active=false` and `visible=false`. The object stays for reuse. -- `Container.setScrollFactor` does not auto-propagate. Pass `true` as the third argument to also update children: `container.setScrollFactor(0, 0, true)`. -- `Group.create()` adds to the Scene display list, but `group.add()` does NOT unless you pass `true` as the second argument. -- Container needs `setSize(width, height)` before `setInteractive()` will work with a hit area — Containers have no implicit size. -</gotchas> -</groups_and_containers> diff --git a/skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md b/skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md deleted file mode 100644 index 6986b2e..0000000 --- a/skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md +++ /dev/null @@ -1,310 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: input keyboard mouse touch - -<input_keyboard_mouse_touch> -Use this when handling user input in Phaser 4: keyboard keys, mouse clicks and movement, touch events, pointer handling, drag and drop, hit areas, interactive objects, and gamepad support. Input can be handled through event listeners or by polling state each frame. - -<quick_start> -```js -class MyScene extends Phaser.Scene { - create() { - // Keyboard: create cursor keys (up, down, left, right, space, shift) - this.cursors = this.input.keyboard.createCursorKeys(); - - // Keyboard: listen for a specific key event - this.input.keyboard.on('keydown-SPACE', (event) => { - console.log('Space pressed'); - }); - - // Pointer: listen for click/tap anywhere on the game canvas - this.input.on('pointerdown', (pointer) => { - console.log('Clicked at', pointer.x, pointer.y); - }); - - // Pointer: make a Game Object interactive and clickable - const sprite = this.add.sprite(400, 300, 'player'); - sprite.setInteractive(); - sprite.on('pointerdown', (pointer, localX, localY, event) => { - console.log('Sprite clicked at local', localX, localY); - }); - } - - update() { - // Poll cursor keys each frame - if (this.cursors.left.isDown) { /* move left */ } - if (this.cursors.right.isDown) { /* move right */ } - if (Phaser.Input.Keyboard.JustDown(this.cursors.space)) { - // fire once per press - } - } -} -``` -</quick_start> - -<core_concepts> -The input plugin (`this.input`) is an `EventEmitter` accessed in any Scene that handles all input for that Scene. Key properties: `enabled` (toggle input processing), `topOnly` (default `true` — only emit events from the top-most object under the pointer), `keyboard` (KeyboardPlugin), `gamepad` (GamepadPlugin), `mouse` (MouseManager), `activePointer` (most recently active Pointer), `mousePointer` (pointers[0], distinct from pointer1 = first touch), `pointer1`–`pointer10`, `dragDistanceThreshold` (px before drag starts), `dragTimeThreshold` (ms before drag starts), `pollRate` (-1 = poll only on movement, 0 = every frame). Key methods: `addPointer(quantity)` for multi-touch (default 2, max 10), `setHitArea*` family (`setHitAreaCircle`, `setHitAreaEllipse`, `setHitAreaRectangle`, `setHitAreaTriangle`, `setHitAreaFromTexture`), `setDraggable(gameObjects, value)`, `makePixelPerfect(alphaTolerance)`. - -A `Pointer` encapsulates both mouse and touch. Key properties: `x`/`y` (screen space), `worldX`/`worldY` (through the most recent Camera), `downX`/`downY`, `upX`/`upY`, `isDown` (any button held), `primaryDown` (primary button held), `button` (0=left, 1=middle, 2=right), `buttons` (bitmask: 1=left, 2=right, 4=middle, 8=back, 16=forward), `wasTouch`, `velocity` (Vector2), `angle`, `distance`, `movementX`/`movementY` (when locked), `deltaX`/`deltaY`/`deltaZ` (wheel), `locked`, `camera`. Methods: `leftButtonDown()` / `rightButtonDown()` / `middleButtonDown()` / `backButtonDown()` / `forwardButtonDown()`, the `*Released()` variants, `getDistance()` / `getDistanceX()` / `getDistanceY()`, `getDuration()`, `getAngle()`, `updateWorldPoint(camera)`, `positionToCamera(camera, output)`. - -Call `gameObject.setInteractive()` to enable input — it uses the texture frame as the hit area by default: -```js -sprite.setInteractive(); // default hit area from texture - -// Custom shape hit areas -sprite.setInteractive(new Phaser.Geom.Circle(32, 32, 32), Phaser.Geom.Circle.Contains); -sprite.setInteractive(new Phaser.Geom.Ellipse(50, 50, 100, 60), Phaser.Geom.Ellipse.Contains); -sprite.setInteractive(new Phaser.Geom.Triangle(0,64,32,0,64,64), Phaser.Geom.Triangle.Contains); -sprite.setInteractive(new Phaser.Geom.Polygon(points), Phaser.Geom.Polygon.Contains); - -// Pixel-perfect hit testing (expensive — use sparingly) -sprite.setInteractive({ pixelPerfect: true, alphaTolerance: 1 }); -sprite.setInteractive(this.input.makePixelPerfect()); -sprite.setInteractive(this.input.makePixelPerfect(150)); // with alpha tolerance - -// Config object with multiple options -sprite.setInteractive({ - draggable: true, dropZone: false, useHandCursor: true, - cursor: 'pointer', pixelPerfect: true, alphaTolerance: 1 -}); - -// Containers must specify a shape or call setSize first -container.setSize(200, 200); -container.setInteractive(); -``` -</core_concepts> - -<keyboard> -Cursor keys return an object with `up`, `down`, `left`, `right`, `space`, `shift` Key objects: -```js -this.cursors = this.input.keyboard.createCursorKeys(); -// In update(): -if (this.cursors.up.isDown) { /* held */ } -if (this.cursors.space.isDown) { /* held */ } -``` - -`addKey` creates a single Key; `addKeys` creates several at once: -```js -const keyW = this.input.keyboard.addKey('W'); // by name -const keySpace = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); // by code -const keyA = this.input.keyboard.addKey('A', true, false); // addKey(key, enableCapture, emitOnRepeat) - -const keys = this.input.keyboard.addKeys('W,S,A,D'); // returns { W, S, A, D } -if (keys.W.isDown) { /* ... */ } - -const named = this.input.keyboard.addKeys({ // object form, custom names - up: Phaser.Input.Keyboard.KeyCodes.W, - down: Phaser.Input.Keyboard.KeyCodes.S, - left: Phaser.Input.Keyboard.KeyCodes.A, - right: Phaser.Input.Keyboard.KeyCodes.D -}); -``` - -Polling vs events: -```js -// Polling in update() -if (keyW.isDown) { /* currently held */ } -if (keyW.isUp) { /* currently released */ } - -// JustDown / JustUp — true only once per press, resets after check -if (Phaser.Input.Keyboard.JustDown(keyW)) { /* fire once */ } -if (Phaser.Input.Keyboard.JustUp(keyW)) { /* released once */ } - -// checkDown with duration — throttled polling -if (this.input.keyboard.checkDown(keySpace, 250)) { /* at most once every 250ms while held */ } - -// Event-driven -this.input.keyboard.on('keydown-SPACE', (event) => { /* ... */ }); -this.input.keyboard.on('keyup-SPACE', (event) => { /* ... */ }); -this.input.keyboard.on('keydown', (event) => { console.log(event.key); }); // any key, native DOM event - -// Event on a Key object itself -const spaceBar = this.input.keyboard.addKey('SPACE'); -spaceBar.on('down', (key, event) => { /* Key object + native event */ }); -spaceBar.on('up', (key, event) => { /* ... */ }); -``` - -Key object properties: `isDown`/`isUp`, `keyCode`, modifier state `altKey`/`ctrlKey`/`shiftKey`/`metaKey`, `duration` (ms held in previous cycle), `timeDown`/`timeUp`, `repeats`, `emitOnRepeat` (fires 'down' on each repeat), `enabled`. - -Prevent browser defaults (captures are global across all Scenes): -```js -this.input.keyboard.addCapture('SPACE,UP,DOWN,LEFT,RIGHT'); -this.input.keyboard.addCapture([ 32, 37, 38, 39, 40 ]); -this.input.keyboard.removeCapture('SPACE'); -``` - -Common KeyCodes (access via `Phaser.Input.Keyboard.KeyCodes.SPACE` etc.): `BACKSPACE(8)`, `TAB(9)`, `ENTER(13)`, `SHIFT(16)`, `CTRL(17)`, `ALT(18)`, `ESC(27)`, `SPACE(32)`, `LEFT(37)`, `UP(38)`, `RIGHT(39)`, `DOWN(40)`, `A-Z(65-90)`, `ZERO-NINE(48-57)`, `F1-F12(112-123)`. - -Key combos listen for a sequence of keys: -```js -this.input.keyboard.createCombo('PHASER'); // string-based -this.input.keyboard.createCombo( // array of codes (Konami) - [ 38, 38, 40, 40, 37, 39, 37, 39, 66, 65, 13 ], - { resetOnMatch: true } -); -this.input.keyboard.on('keycombomatch', (keyCombo, event) => { console.log('Combo matched!'); }); -``` -`createCombo(keys, config)` — `keys` is a string (each char a key) or array of codes/Key objects. Config: `resetOnWrongKey` (default true), `maxKeyDelay` (max ms between presses, 0 = no limit), `resetOnMatch` (default false), `deleteOnMatch` (default false). -</keyboard> - -<mouse_and_pointer> -Scene-level pointer events fire anywhere on the canvas: -```js -this.input.on('pointerdown', (pointer, currentlyOver) => { - // currentlyOver: array of interactive Game Objects under the pointer -}); -this.input.on('pointerup', (pointer, currentlyOver) => { /* ... */ }); -this.input.on('pointermove', (pointer, currentlyOver) => { /* ... */ }); -this.input.on('wheel', (pointer, currentlyOver, deltaX, deltaY, deltaZ) => { /* ... */ }); -``` - -Game Object pointer events require `setInteractive`: -```js -sprite.setInteractive(); -sprite.on('pointerdown', (pointer, localX, localY, event) => { - // localX/localY relative to the object's top-left; event.stopPropagation() halts further dispatch -}); -sprite.on('pointerup', (pointer, localX, localY, event) => { /* ... */ }); -sprite.on('pointermove', (pointer, localX, localY, event) => { /* ... */ }); -sprite.on('pointerover', (pointer, localX, localY, event) => { /* ... */ }); -sprite.on('pointerout', (pointer, event) => { /* ... */ }); -sprite.on('wheel', (pointer, deltaX, deltaY, deltaZ, event) => { /* ... */ }); -``` - -Pointer events fire at three levels: (1) Game Object — `gameObject.on('pointerdown', (pointer, localX, localY, event) => {})`; (2) Scene per-object — `this.input.on('gameobjectdown', (pointer, gameObject, event) => {})`; (3) Scene global — `this.input.on('pointerdown', (pointer, currentlyOver) => {})`. - -Right-click and context menu: -```js -this.input.on('pointerdown', (pointer) => { - if (pointer.rightButtonDown()) { /* right-click */ } -}); -this.input.mouse.disableContextMenu(); -``` - -Pointer lock (FPS-style mouse capture): -```js -this.input.on('pointerdown', () => { this.input.mouse.requestPointerLock(); }); -this.input.on('pointerlockchange', (event, locked) => { /* locked: boolean */ }); -// Read relative movement while locked: pointer.movementX, pointer.movementY -``` - -Debug, topOnly, and multi-touch: -```js -this.input.enableDebug(gameObject); // visualize hit areas -this.input.enableDebug(gameObject, 0xff00ff); -this.input.removeDebug(gameObject); - -this.input.topOnly = false; // let all objects under pointer receive events -this.input.setTopOnly(false); -``` -Multi-touch: set `config.input.activePointers` to reserve slots at startup, or call `this.input.addPointer(num)` at runtime. Access via `this.input.pointer1` through `pointer10`. -</mouse_and_pointer> - -<drag_and_drop> -```js -const sprite = this.add.sprite(400, 300, 'item'); -sprite.setInteractive(); -this.input.setDraggable(sprite); // or sprite.setInteractive({ draggable: true }); - -// Scene-level drag events -this.input.on('dragstart', (pointer, gameObject) => { gameObject.setTint(0xff0000); }); -this.input.on('drag', (pointer, gameObject, dragX, dragY) => { - gameObject.x = dragX; - gameObject.y = dragY; -}); -this.input.on('dragend', (pointer, gameObject) => { gameObject.clearTint(); }); - -// Drop zones -const zone = this.add.zone(600, 300, 200, 200).setRectangleDropZone(200, 200); -this.input.on('drop', (pointer, gameObject, dropZone) => { - gameObject.x = dropZone.x; - gameObject.y = dropZone.y; -}); -this.input.on('dragenter', (pointer, gameObject, dropZone) => { /* ... */ }); -this.input.on('dragleave', (pointer, gameObject, dropZone) => { /* ... */ }); -this.input.on('dragover', (pointer, gameObject, dropZone) => { /* ... */ }); -``` - -Game Object-level drag events are also available: -```js -sprite.on('drag', (pointer, dragX, dragY) => { sprite.x = dragX; sprite.y = dragY; }); -sprite.on('dragstart', (pointer, dragX, dragY) => { /* ... */ }); -sprite.on('dragend', (pointer, dragX, dragY) => { /* ... */ }); -sprite.on('drop', (pointer, dropZone) => { /* ... */ }); -``` - -Drag thresholds: -```js -this.input.dragDistanceThreshold = 16; // must move 16px before drag starts -this.input.dragTimeThreshold = 200; // must hold 200ms before drag starts -``` -</drag_and_drop> - -<gamepad> -Enable gamepads in the game config, then access via `this.input.gamepad`. Pads are `pad1` through `pad4`: -```js -const config = { input: { gamepad: true } }; - -this.input.gamepad.once('connected', (pad) => { console.log('Gamepad connected:', pad.id); }); -if (this.input.gamepad.total > 0) { - const pad = this.input.gamepad.pad1; -} -``` - -Polling gamepad state in update(): -```js -update() { - const pad = this.input.gamepad.pad1; - if (!pad) return; - - if (pad.up) { /* d-pad up */ } // D-pad (boolean) - if (pad.down) { /* ... */ } - if (pad.left) { /* ... */ } - if (pad.right) { /* ... */ } - - if (pad.A) { /* bottom button (Xbox A / PS X) */ } // face buttons (boolean), Xbox naming - if (pad.B) { /* right (Xbox B / PS Circle) */ } - if (pad.X) { /* left (Xbox X / PS Square) */ } - if (pad.Y) { /* top (Xbox Y / PS Triangle) */ } - - if (pad.L1 > 0) { /* left shoulder (LB) */ } // shoulders (float 0-1) - if (pad.L2 > 0) { /* left trigger (LT) */ } - if (pad.R1 > 0) { /* right shoulder (RB) */ } - if (pad.R2 > 0) { /* right trigger (RT) */ } - - const lx = pad.leftStick.x; // analog sticks (Vector2, -1 to 1) - const ly = pad.leftStick.y; - const rx = pad.rightStick.x; - const ry = pad.rightStick.y; - - pad.getAxisValue(0); // raw axis (float) - pad.getButtonValue(0); // raw button (float 0-1) - pad.isButtonDown(0); // boolean - pad.setAxisThreshold(0.1); // ignore values below threshold -} -``` - -Gamepad events: -```js -// Plugin-level (any gamepad) -this.input.gamepad.on('connected', (pad, event) => { /* ... */ }); -this.input.gamepad.on('disconnected', (pad, event) => { /* ... */ }); -this.input.gamepad.on('down', (pad, button, value) => { /* any button on any pad */ }); -this.input.gamepad.on('up', (pad, button, value) => { /* ... */ }); - -// Gamepad-instance -pad.on('down', (index, value, button) => { /* button on this specific pad */ }); -pad.on('up', (index, value, button) => { /* ... */ }); -``` - -Vibration (experimental, hardware/browser dependent): -```js -if (pad.vibration) { - pad.vibration.playEffect('dual-rumble', { - duration: 200, strongMagnitude: 1.0, weakMagnitude: 0.5 - }); -} -``` -</gamepad> -</input_keyboard_mouse_touch> diff --git a/skills/base44-platform-games/references/phaser/loading_assets.md b/skills/base44-platform-games/references/phaser/loading_assets.md deleted file mode 100644 index 4f6fa8b..0000000 --- a/skills/base44-platform-games/references/phaser/loading_assets.md +++ /dev/null @@ -1,265 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: loading assets - -<loading_assets> -Use this when loading assets in Phaser 4: the Loader plugin (`this.load`) for images, spritesheets, atlases, audio, JSON, tilemaps, bitmap fonts, and more. Assets are queued in `preload()`, loaded in parallel, and placed into global caches accessible by every Scene. - -<quick_start> -```js -class GameScene extends Phaser.Scene { - preload() { - this.load.image('logo', 'assets/logo.png'); - } - create() { - this.add.image(400, 300, 'logo'); - } -} -``` -Assets loaded in `preload()` are guaranteed ready when `create()` runs. The Loader starts automatically during the preload phase. -</quick_start> - -<core_concepts> -Every Scene can define a `preload()` method. Queued files don't load immediately — the Loader starts when `preload()` completes and waits for all files before calling `create()`: -```js -preload() { - this.load.image('sky', 'assets/sky.png'); - this.load.spritesheet('dude', 'assets/dude.png', { frameWidth: 32, frameHeight: 48 }); - this.load.audio('jump', 'assets/jump.mp3'); -} -create() { - this.add.image(400, 300, 'sky'); - this.add.sprite(100, 450, 'dude'); - this.sound.play('jump'); -} -``` - -Loading outside `preload()` (e.g. in `create()` or in response to a user action) requires manually starting the Loader: -```js -create() { - this.load.image('extra', 'assets/extra.png'); - this.load.once('complete', () => { this.add.image(400, 300, 'extra'); }); - this.load.start(); -} -``` - -URL resolution is `baseURL + path + filename`, set via game config or at runtime. The prefix is prepended to the cache key, NOT the URL. Absolute URLs bypass path/baseURL: -```js -preload() { - this.load.setBaseURL('https://cdn.example.com/'); - this.load.setPath('assets/images/'); - this.load.setPrefix('LEVEL1.'); - // Loads from https://cdn.example.com/assets/images/hero.png, cached as 'LEVEL1.hero' - this.load.image('hero', 'hero.png'); - this.load.image('cloud', 'https://other-server.com/cloud.png'); // absolute, bypasses path -} -``` -These can also be set in the game config under `loader: { baseURL, path, prefix, maxParallelDownloads, crossOrigin, responseType, async, timeout, maxRetries, imageLoadType }`. - -Assets go into global game-level caches, not per-Scene — an image loaded in one Scene is available everywhere. Textures go into `this.textures` (the Texture Manager); other data goes into `this.cache` sub-caches (`this.cache.json`, `this.cache.audio`, `this.cache.xml`, etc.). - -The Loader emits events throughout the lifecycle — use these for progress bars and loading screens: -```js -preload() { - this.load.on('progress', (value) => { /* value 0..1 */ }); - this.load.on('complete', () => { console.log('All assets loaded'); }); - this.load.on('loaderror', (file) => { console.warn('Failed to load:', file.key); }); - this.load.image('bg', 'assets/bg.png'); -} -``` -</core_concepts> - -<common_patterns> -Images and sprite sheets: -```js -preload() { - this.load.image('star', 'assets/star.png'); - this.load.image('brick', ['assets/brick.png', 'assets/brick_n.png']); // [texture, normalMap] - this.load.spritesheet('explosion', 'assets/explosion.png', { - frameWidth: 64, frameHeight: 64, startFrame: 0, endFrame: 23, margin: 0, spacing: 0 - }); - this.load.svg('logo', 'assets/logo.svg', { width: 400, height: 400 }); // optional rasterize size -} -``` - -Audio — always provide multiple formats for cross-browser support: -```js -preload() { - this.load.audio('bgm', 'assets/music.mp3'); - this.load.audio('bgm', ['assets/music.ogg', 'assets/music.mp3']); // first supported wins - this.load.audioSprite('sfx', 'assets/sfx.json', ['assets/sfx.ogg', 'assets/sfx.mp3']); -} -``` - -JSON and tilemaps: -```js -preload() { - this.load.json('levelData', 'assets/level1.json'); - this.load.json('enemies', 'assets/data.json', 'enemies'); // dataKey extracts a sub-object - this.load.tilemapTiledJSON('map', 'assets/map.json'); // Tiled JSON - this.load.tilemapCSV('csvmap', 'assets/level.csv'); - this.load.tilemapImpact('impactmap', 'assets/level.js'); -} -create() { - const data = this.cache.json.get('levelData'); - const map = this.make.tilemap({ key: 'map' }); -} -``` - -Atlases: -```js -preload() { - this.load.atlas('sprites', 'assets/sprites.png', 'assets/sprites.json'); // TexturePacker JSON - this.load.atlasXML('ui', 'assets/ui.png', 'assets/ui.xml'); // Starling/Sparrow - this.load.multiatlas('world', 'assets/world.json', 'assets/'); // split across textures - this.load.unityAtlas('chars', 'assets/chars.png', 'assets/chars.txt'); - this.load.aseprite('knight', 'assets/knight.png', 'assets/knight.json'); -} -create() { - this.add.sprite(400, 300, 'sprites', 'walk_01'); -} -``` - -Bitmap fonts (need both a texture and an XML/JSON data file), video, and web fonts: -```js -preload() { - this.load.bitmapFont('pixels', 'assets/font.png', 'assets/font.xml'); - this.load.video('intro', 'assets/intro.mp4'); - this.load.video('bg_loop', 'assets/loop.mp4', true); // third arg = noAudio - this.load.font('myFont', 'assets/myfont.ttf', 'truetype'); - this.load.font('boldFont', 'assets/bold.woff2', 'woff2', { weight: 'bold', style: 'normal' }); -} -create() { - this.add.bitmapText(100, 100, 'pixels', 'Hello World', 32); -} -``` - -Pack files — a JSON manifest describing multiple assets to load at once: -```js -preload() { this.load.pack('pack1', 'assets/pack.json'); } -``` -```json -{ - "section1": { - "baseURL": "assets/", - "files": [ - { "type": "image", "key": "bg", "url": "bg.png" }, - { "type": "atlas", "key": "chars", "textureURL": "chars.png", "atlasURL": "chars.json" } - ] - } -} -``` -Pack data can also be inline instead of from a URL: -```js -preload() { - this.load.pack('manifest', { - section1: { - prefix: '', path: 'assets/', defaultType: 'image', - files: [ - { type: 'image', key: 'bg', url: 'bg.png' }, - { type: 'spritesheet', key: 'player', url: 'player.png', frameConfig: { frameWidth: 32, frameHeight: 32 } } - ] - } - }); -} -``` - -Progress bar: -```js -preload() { - const progressBar = this.add.graphics(); - const progressBox = this.add.graphics(); - progressBox.fillStyle(0x222222, 0.8); - progressBox.fillRect(240, 270, 320, 50); - - this.load.on('progress', (value) => { - progressBar.clear(); - progressBar.fillStyle(0xffffff, 1); - progressBar.fillRect(250, 280, 300 * value, 30); - }); - this.load.on('complete', () => { progressBar.destroy(); progressBox.destroy(); }); - - this.load.image('sky', 'assets/sky.png'); - // ... more assets -} -``` - -All `this.load` methods accept positional arguments or a single config object, and an array of config objects to batch-load. Available file types: textures (`image`, `spritesheet`, `atlas`, `atlasXML`, `multiatlas`, `unityAtlas`, `aseprite`, `svg`, `htmlTexture`, `texture`), audio/video (`audio`, `audioSprite`, `video`), data (`json`, `xml`, `text`, `binary`, `html`, `css`, `glsl`), fonts (`bitmapFont`, `font`), tilemaps (`tilemapTiledJSON`, `tilemapCSV`, `tilemapImpact`), and `animation`, `pack`, `script`, `scripts`, `plugin`, `scenePlugin`, `sceneFile`. -</common_patterns> - -<events> -All events are emitted on the Loader instance (`this.load`): -- `'addfile'` `(key, type, loader, file)` — a file was added to the queue -- `'start'` `(loader)` — Loader started, progress is zero -- `'load'` `(file)` — a single file finished downloading (before processing/caching) -- `'fileprogress'` `(file, percentComplete)` — per-file download progress (0-1); only fires if the browser provides `lengthComputable` -- `'progress'` `(value)` — overall load progress (0-1) -- `'postprocess'` `(loader)` — all files loaded and processed, before internal cleanup -- `'filecomplete'` `(key, type, data)` — any file finished loading and processing -- `'filecomplete-{type}-{key}'` `(key, type, data)` — a specific file finished (e.g. `'filecomplete-image-hero'`) -- `'loaderror'` `(file)` — a file failed to load -- `'complete'` `(loader, totalComplete, totalFailed)` — all files done - -Lifecycle order: `start` → `fileprogress` (repeats per file) → `load` → `filecomplete-{type}-{key}` → `filecomplete` → `progress` → `postprocess` → `complete`. -</events> - -<advanced> -Scene payload — load files before `preload()` runs by defining a `pack` in the Scene constructor (useful for progress-bar assets): -```js -class BootScene extends Phaser.Scene { - constructor() { - super({ - key: 'BootScene', - pack: { files: [ { type: 'image', key: 'bar', url: 'loaderBar.png' } ] } - }); - } - // 'bar' is already available in preload() -} -``` - -Adding files mid-load — chain dependent loads via the per-file completion event: -```js -preload() { - this.load.json('level1', 'level1.json'); - this.load.on('filecomplete-json-level1', (key, type, data) => { - this.load.image(data.images); - this.load.spritesheet(data.spritesheets); - }); -} -``` - -Cache system — loaded assets go into global caches shared across Scenes. Textures live in `this.textures`; other types in typed sub-caches under `this.cache` (`text`, `json`, `audio`, `binary`, `shader`, `xml`): -```js -const json = this.cache.json.get('levelData'); -const text = this.cache.text.get('dialogue'); - -this.cache.json.exists('levelData'); // or .has('levelData') -this.textures.exists('hero'); - -this.textures.remove('hero'); // free memory -this.cache.audio.remove('bgm'); - -this.textures.on('addtexture-mapKey', (texture) => { - // Another scene loaded 'mapKey' — now available here -}); -``` -</advanced> - -<gotchas> -- Keys must be unique within their type. A second `this.load.image('bg', ...)` is silently ignored if `'bg'` already exists. Remove the old texture (`this.textures.remove`) first to replace it. -- A sprite sheet is not an atlas. Use `spritesheet()` for fixed-size grids referenced by index; use `atlas()` for packed textures with named frames. -- Outside `preload()` the Loader does NOT auto-start — call `this.load.start()` manually. -- Path must end with `/`. `this.load.setPath()` appends the slash automatically; setting `this.load.path` directly requires the trailing slash yourself. -- Always provide multiple audio formats (OGG + MP3 minimum) — the Loader picks the first the browser supports. -- Pack file sections can each override `baseURL`/`path`/`prefix`; those apply only within the section and are restored after. -- The cache key includes the prefix. With `setPrefix('MENU.')`, an image keyed `'bg'` is cached as `'MENU.bg'` — reference it by the full key. -- `maxRetries` (default 2) is captured per-file at creation time; changing it afterward has no effect on already-queued files. -- `imageLoadType: 'HTMLImageElement'` in the loader config uses `<img>` tag loading instead of XHR blobs, which can help with CORS in some environments. -- `file://` and `capacitor://` are treated as local schemes (via `localSchemes`) and skip CORS headers. -- Set `crossOrigin: 'anonymous'` (or `this.load.setCORS('anonymous')`) when loading from another domain, especially for WebGL textures. -- Keys are case-sensitive and scoped per type — `'Player'` ≠ `'player'`; an image and audio asset can share key `'player'`. -- Scene `update()` does NOT fire during preload (it's paused), but `preupdate`, `postupdate`, and `render` still fire. -- Progress can decrease — if files are added mid-load (e.g. via `filecomplete` chaining), the value drops because total count rose. -</gotchas> -</loading_assets> diff --git a/skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md b/skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md deleted file mode 100644 index e9f12c0..0000000 --- a/skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md +++ /dev/null @@ -1,50 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: mesh and 3d elements - -<mesh_and_3d_elements> -Use this when a 2D Phaser game wants a 3D LOOK or depth flourishes without leaving 2D — Phaser stays the only engine (physics, input, scenes, collision are all unchanged). These are pseudo-3D tricks: plain 2D, no new machinery, not real 3D. - -Note: Phaser's `Mesh`/`Plane` game objects from v3 (`this.add.mesh()`, `Phaser.Geom.Mesh.GenerateGridVerts`, `panZ`, `modelRotation`) were REMOVED in Phaser 4 — there is no Phaser-native vehicle for unlit 3D accents anymore. Small flourishes that used to lean on Mesh (card flips, tilting panels) are done with pseudo-3D scale/angle tweens, frame swaps, or a sheared Graphics quad. - -<tier1_pseudo_3d> -Fake depth with plain 2D: -```js -// Depth scaling: things lower on screen are "closer" -sprite.setScale(0.5 + (sprite.y / this.scale.height) * 0.7); -sprite.setDepth(sprite.y); - -// Parallax background layers (farther = slower) -this.add.tileSprite(0, 0, w, h, 'far-hills').setOrigin(0).setScrollFactor(0.2); -this.add.tileSprite(0, 0, w, h, 'near-trees').setOrigin(0).setScrollFactor(0.6); - -// Isometric placement + draw-order sorting -const screenX = (tileX - tileY) * TILE_W / 2; -const screenY = (tileX + tileY) * TILE_H / 2; -obj.setDepth(screenY); -``` -Floor-stripe "mode 7" feel: scale + tint rows of tileSprites by distance instead of attempting real projection. - -Fake a 3D card flip without any Mesh — tween `scaleX` through 0 and swap the texture at the midpoint: -```js -this.tweens.add({ - targets: card, - scaleX: 0, - duration: 250, - ease: 'Cubic.easeIn', - onComplete: () => { - card.setTexture('card-front'); - this.tweens.add({ targets: card, scaleX: 1, duration: 250, ease: 'Cubic.easeOut' }); - }, -}); -``` -Tilting panels read well as a short `angle` + `scaleY` tween, a frame swap between pre-drawn angles, or a sheared quad drawn with Graphics (`fillPoints`). (Phaser game objects have no skew transform — a `skewX` tween renders nothing.) -</tier1_pseudo_3d> - -<common_mistakes> -- Reaching for `this.add.mesh()` / `Phaser.Geom.Mesh` / `panZ` / `modelRotation` — the Phaser Mesh and Plane objects were removed in v4. Use a pseudo-3D tween, frame swap, or sheared Graphics quad instead. -- Expecting a `skewX` tween to do anything — Phaser game objects have no skew transform; shear with a Graphics `fillPoints` quad instead. -- Forgetting depth sorting (`setDepth`) on overlapping pseudo-3D sprites — closer objects then draw in front of farther ones. -</common_mistakes> -</mesh_and_3d_elements> diff --git a/skills/base44-platform-games/references/phaser/particles.md b/skills/base44-platform-games/references/phaser/particles.md deleted file mode 100644 index 94dd3fc..0000000 --- a/skills/base44-platform-games/references/phaser/particles.md +++ /dev/null @@ -1,290 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: particles - -<particles> -Use this when creating particle effects in Phaser 4: ParticleEmitter creation and configuration, emitter op value formats, gravity wells, emission and death zones, flow vs burst modes, following game objects, and particle callbacks. - -<quick_start> -```js -// In a Scene's create() method: - -// Basic continuous emitter (flow mode) -const emitter = this.add.particles(400, 300, 'flares', { - frame: 'red', - speed: 200, - lifespan: 2000, - scale: { start: 1, end: 0 }, - alpha: { start: 1, end: 0 }, - gravityY: 150 -}); - -// One-shot burst (explode mode) -const burst = this.add.particles(400, 300, 'flares', { - frame: 'blue', - speed: { min: 100, max: 300 }, - lifespan: 1000, - scale: { start: 0.5, end: 0 }, - emitting: false // don't auto-start -}); -burst.explode(20); // emit 20 particles at once -``` -</quick_start> - -<core_concepts> -`ParticleEmitter` extends `GameObject` and is added directly to the display list — it is both a positionable/scalable/maskable game object and the emitter itself. There is no separate manager; `this.add.particles()` returns a `ParticleEmitter`. -```js -this.add.particles(x, y, texture, config); -// x, y: world position (both optional, default 0) -// texture: string key or Texture instance -// config: ParticleEmitterConfig object (optional, can call setConfig later) -``` -Mixins (AlphaSingle, BlendMode, Depth, Lighting, Mask, RenderNodes, ScrollFactor, Texture, Transform, Visible) mean you can call `setPosition()`, `setScale()`, `setDepth()`, `setBlendMode()`, `setMask()`, `setScrollFactor()`, etc. - -A `Particle` is a lightweight, internally-pooled object owned by its emitter — you never create them manually. Key properties: `x`, `y`, `velocityX/Y`, `accelerationX/Y`, `scaleX/Y`, `alpha`, `angle`, `rotation`, `tint`, `life` (total ms), `lifeCurrent` (remaining ms), `lifeT` (0-1 normalized), `bounce`, `delayCurrent`, `holdCurrent`. - -Most config properties (speed, scale, alpha, angle, x, y, etc.) accept flexible EmitterOp value formats: -```js -x: 400 // static value -x: [100, 200, 300, 400] // random pick from array -x: { min: 100, max: 700 } // random float in range -x: { min: 100, max: 700, int: true } // random integer -x: { random: [100, 700] } // random integer shorthand -scale: { start: 0, end: 1 } // ease over lifetime (default linear) -scale: { start: 0, end: 1, ease: 'bounce.out' } // custom ease -scale: { start: 4, end: 0.5, random: true } // random start, ease to end -x: { values: [50, 500, 200, 800], interpolation: 'catmull' } // interpolation -x: { steps: 32, start: 0, end: 576 } // stepped sequential -x: { steps: 32, start: 0, end: 576, yoyo: true } // stepped with yoyo -x: { // custom callbacks - onEmit: (particle, key, t, value) => value, - onUpdate: (particle, key, t, value) => value -} -x: (particle, key, t, value) => value + 50 // emit-time callback shorthand -``` -Emit-only properties (no onUpdate): `angle`, `delay`, `hold`, `lifespan`, `quantity`, `speedX`, `speedY`. Emit + Update (support start/end and onUpdate): `accelerationX/Y`, `alpha`, `bounce`, `maxVelocityX/Y`, `moveToX/Y`, `rotate`, `scaleX/Y`, `tint`, `x`, `y`. - -Flow vs Explode: in flow mode (`frequency >= 0`) the emitter emits `quantity` particles every `frequency` ms — default is `frequency: 0` (every frame) with `emitting: true`. In explode mode (`frequency = -1`) it emits a batch all at once, then stops. -```js -emitter.flow(100, 5); // 5 particles every 100ms -emitter.flow(100, 5, 50); // auto-stop after 50 total -emitter.explode(30, 200, 400); // burst 30 at position -emitter.explode(30); // burst at emitter position -``` -</core_concepts> - -<common_patterns> -Scale, alpha, and color over lifetime: -```js -this.add.particles(400, 300, 'spark', { - lifespan: 2000, speed: 100, - scale: { start: 1, end: 0, ease: 'power2' }, - alpha: { start: 1, end: 0, ease: 'cubic.in' } -}); -``` - -Color interpolation — the `color` array interpolates over lifetime and overrides `tint`: -```js -this.add.particles(400, 300, 'spark', { - lifespan: 2000, speed: 100, scale: { start: 0.5, end: 0 }, - color: [0xfacc22, 0xf89800, 0xf83600, 0x9f0404], colorEase: 'quad.out' -}); -``` - -Tinting: -```js -this.add.particles(400, 300, 'spark', { tint: 0xff0000 }); // static -this.add.particles(400, 300, 'spark', { tint: { start: 0xffffff, end: 0xff0000 } }); // over lifetime -``` - -Gravity wells apply inverse-square gravitational force, pulling (or repelling with negative `power`) particles toward a point: -```js -const emitter = this.add.particles(400, 300, 'spark', { - speed: 100, lifespan: 4000, scale: { start: 0.4, end: 0 }, quantity: 2 -}); -const well = emitter.createGravityWell({ x: 400, y: 300, power: 2, epsilon: 100, gravity: 50 }); -well.x = 300; -well.power = -1; // negative = repel - -// Or create manually and add -const well2 = new Phaser.GameObjects.Particles.GravityWell(500, 200, 3, 100, 50); -emitter.addParticleProcessor(well2); -emitter.removeParticleProcessor(well2); -``` -GravityWell properties: `x`, `y` (world position), `power` (force strength, negative to repel), `epsilon` (min distance for force calc, default 100), `gravity` (gravitational constant, default 50), `active`. Constructor: `new GravityWell(x, y, power, epsilon, gravity)` or `new GravityWell({ x, y, power, epsilon, gravity })`. - -Emission zones (random) — a RandomZone spawns particles at random positions within a shape. The source needs a `getRandomPoint(point)` method; all Phaser geometry classes support it, or provide a custom source: -```js -this.add.particles(400, 300, 'spark', { - speed: 50, lifespan: 2000, - emitZone: { type: 'random', source: new Phaser.Geom.Circle(0, 0, 100) } -}); -emitter.addEmitZone({ - type: 'random', - source: { - getRandomPoint: (point) => { - const a = Math.random() * Math.PI * 2; - point.x = Math.cos(a) * 100; - point.y = Math.sin(a) * 50; - return point; - } - } -}); -``` - -Emission zones (edge) — an EdgeZone places particles sequentially along shape edges. The source needs `getPoints(quantity, stepRate)`; Curves, Paths, and all geometry shapes support it: -```js -this.add.particles(400, 300, 'spark', { - lifespan: 1500, speed: 20, - emitZone: { - type: 'edge', - source: new Phaser.Geom.Circle(0, 0, 150), - quantity: 48, // number of points on edge (use 0 with stepRate instead) - yoyo: false, // reverse direction at ends - seamless: true // remove duplicate endpoint - } -}); -emitter.addEmitZone({ type: 'edge', source: geom, quantity: 50, yoyo: false, seamless: true }); -``` -Multiple emission zones: pass an array to `emitZone` or call `addEmitZone()` repeatedly. Zones iterate in sequence; the `total` property controls how many particles emit before rotating to the next zone (-1 = never rotate). -```js -this.add.particles(400, 300, 'spark', { - emitZone: [ - { type: 'random', source: new Phaser.Geom.Circle(0, 0, 50) }, - { type: 'random', source: new Phaser.Geom.Circle(200, 0, 50) } - ] -}); -``` - -Death zones kill particles on entering (or leaving) a region. The source needs a `contains(x, y)` method: -```js -// Kill particles entering a rectangle -this.add.particles(400, 100, 'spark', { - speed: 200, lifespan: 5000, gravityY: 100, - deathZone: { type: 'onEnter', source: new Phaser.Geom.Rectangle(300, 400, 200, 50) } -}); -// Kill particles leaving a circle (confine to area) -this.add.particles(400, 300, 'spark', { - speed: 100, lifespan: 5000, - deathZone: { type: 'onLeave', source: new Phaser.Geom.Circle(400, 300, 150) } -}); -emitter.addDeathZone({ type: 'onEnter', source: { contains: (x, y) => x > 600 && y > 400 } }); -``` - -Following a game object: -```js -const player = this.add.sprite(100, 100, 'player'); -const emitter = this.add.particles(0, 0, 'spark', { speed: 50, lifespan: 800, scale: { start: 0.5, end: 0 } }); -emitter.startFollow(player); // follow position -emitter.startFollow(player, 10, -20); // with offset -emitter.startFollow(player, 0, 0, true); // track visibility too -emitter.stopFollow(); -// Or via config: -this.add.particles(0, 0, 'spark', { follow: player, followOffset: { x: 0, y: -20 } }); -``` - -Particle callbacks and iteration: -```js -const emitter = this.add.particles(400, 300, 'spark', { - speed: 100, lifespan: 2000, - emitCallback: (particle, emitter) => { /* on emit */ }, - deathCallback: (particle) => { /* on death */ } -}); -emitter.onParticleEmit((particle, emitter) => { /* ... */ }); -emitter.onParticleDeath((particle) => { /* ... */ }); -emitter.forEachAlive((particle, emitter) => { /* particle.x, particle.lifeT */ }); -``` - -Duration, stopAfter, and advance (pre-warm): -```js -this.add.particles(400, 300, 'spark', { speed: 100, duration: 3000 }); // auto-stop after 3s; alive particles finish -this.add.particles(400, 300, 'spark', { speed: 100, stopAfter: 50 }); // emit exactly 50 then stop -this.add.particles(400, 300, 'spark', { speed: 100, lifespan: 2000, advance: 2000 }); // fast-forward 2s on creation -// Or manually: emitter.fastForward(2000, 50); -``` - -Particle bounds (bounce): -```js -this.add.particles(400, 300, 'spark', { - speed: 200, lifespan: 5000, bounce: 0.8, - bounds: { x: 100, y: 100, width: 600, height: 400 }, - collideLeft: true, collideRight: true, collideTop: true, collideBottom: true -}); -// Or: emitter.addParticleBounds(100, 100, 600, 400); -``` - -Texture frames and animations: -```js -this.add.particles(400, 300, 'flares', { frame: ['red', 'green', 'blue'] }); // random frame per particle -this.add.particles(400, 300, 'flares', { frame: { frames: ['red', 'green', 'blue'], cycle: true, quantity: 4 } }); // sequential cycling -this.add.particles(400, 300, 'explosion', { anim: 'explode_anim', lifespan: 1000 }); // play anim over lifetime -this.add.particles(400, 300, 'sheet', { anim: { anims: ['fire', 'smoke'], cycle: false, quantity: 1 } }); // multiple anims, random -``` - -Sorting: -```js -this.add.particles(400, 300, 'spark', { sortProperty: 'y', sortOrderAsc: true }); -// Or: sortCallback: (a, b) => a.y - b.y -``` - -Custom particle processor — extend `ParticleProcessor` for per-particle logic each frame via `update(particle, delta, step, t)`: -```js -class WindProcessor extends Phaser.GameObjects.Particles.ParticleProcessor { - constructor (windX, windY) { super(0, 0); this.windX = windX; this.windY = windY; } - update (particle, delta, step, t) { - particle.velocityX += this.windX * step; - particle.velocityY += this.windY * step; - } -} -emitter.addParticleProcessor(new WindProcessor(0.5, 0)); -``` - -Custom particle class — extend `Particle`, override `update`, and set via `particleClass`: -```js -class TrailParticle extends Phaser.GameObjects.Particles.Particle { - update (delta, step, processors) { - const result = super.update(delta, step, processors); - this.alpha = this.lifeT; // custom: alpha matches life progress - return result; // must return true if particle is still alive - } -} -this.add.particles(400, 300, 'spark', { particleClass: TrailParticle, speed: 100, lifespan: 2000 }); -``` - -Key ParticleEmitter methods — Lifecycle: `start(advance?, duration?)`, `stop(kill?)`, `pause()`, `resume()`, `flow(frequency, count?, stopAfter?)`, `explode(count?, x?, y?)`, `emitParticleAt(x?, y?, count?)`, `emitParticle(count?, x?, y?)`, `fastForward(time, delta?)`. Config: `setConfig`, `updateConfig`. Following: `startFollow`, `stopFollow`. Zones: `addEmitZone`/`removeEmitZone`/`clearEmitZones`, `addDeathZone`/`removeDeathZone`/`clearDeathZones`. Processors: `createGravityWell`, `addParticleProcessor`, `removeParticleProcessor`, `getProcessors`. Bounds: `addParticleBounds`. Callbacks/iteration: `onParticleEmit`, `onParticleDeath`, `killAll`, `forEachAlive`, `forEachDead`. Counts: `getAliveParticleCount`, `getDeadParticleCount`, `getParticleCount`, `atLimit`, `reserve`. Property setters: `setParticleSpeed`, `setParticleScale`, `setParticleGravity`, `setParticleAlpha`, `setParticleTint`, `setParticleLifespan`, `setEmitterAngle`, `setQuantity`, `setFrequency`, `setRadial`, `setEmitterFrame`, `setAnim`. Sorting: `setSortProperty`, `setSortCallback`, `depthSort`. Utility: `getBounds`, `overlap`. -</common_patterns> - -<events> -All events are emitted on the `ParticleEmitter` instance: -- `'start'` `(emitter)` — `start()` is called and the emitter begins emitting -- `'stop'` `(emitter)` — `stop()` is called, or a duration/stopAfter limit is reached -- `'complete'` `(emitter)` — the final alive particle dies after the emitter has stopped -- `'explode'` `(emitter, particle)` — `explode()` is called -- `'deathzone'` `(emitter, particle, zone)` — a death zone kills a particle - -```js -emitter.on('stop', (emitter) => { /* stopped emitting */ }); -emitter.on('complete', (emitter) => { /* all particles dead */ }); -emitter.on('deathzone', (emitter, particle, zone) => { /* ... */ }); -``` -</events> - -<gotchas> -- No ParticleEmitterManager — removed in v3.60. `this.add.particles()` returns a `ParticleEmitter` directly. -- `speed` vs `speedX`/`speedY`: `speed` sets speedX and deactivates speedY (radial mode). `speedX`/`speedY` switches to point mode (`radial: false`). -- `scale` vs `scaleX`/`scaleY`: `scale` applies to scaleX and deactivates scaleY. Use both for non-uniform scaling. -- `color` overrides `tint` — they're mutually exclusive; the color array wins. -- `moveToX`/`moveToY`: both must be set to activate; overrides `angle` and `speed`. -- `emitting` vs `active`: `emitting: false` stops new particles but alive ones still update; `active: false` freezes the entire emitter. -- `stop` vs `complete`: `'stop'` fires when emission stops; `'complete'` fires when the last alive particle dies. -- `frequency: 0` means emit every frame (max rate), not "never." Use `emitting: false` to prevent emission. -- `frequency: -1` puts the emitter in explode mode — it won't flow automatically; use `explode()` for bursts. -- `hold` keeps a particle visible and frozen for the given ms after its lifespan expires, before it dies — useful for trail/lingering effects. -- `advance` fast-forwards by simulating the given ms on creation so particles are already visible on the first frame. -- `reserve(count)` (or `reserve` in config) pre-allocates particle objects upfront, avoiding GC spikes during gameplay. -- Zone source method requirements: RandomZone needs `getRandomPoint(point)`, EdgeZone needs `getPoints(quantity, stepRate)`, DeathZone needs `contains(x, y)`. -- `maxParticles` limits total objects (not alive count); use `maxAliveParticles` for the visible limit. -- The emitter needs a valid texture key; use the `frame` config for multi-frame textures. -</gotchas> -</particles> diff --git a/skills/base44-platform-games/references/phaser/physics_arcade.md b/skills/base44-platform-games/references/phaser/physics_arcade.md deleted file mode 100644 index 204a44d..0000000 --- a/skills/base44-platform-games/references/phaser/physics_arcade.md +++ /dev/null @@ -1,256 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: physics arcade - -<physics_arcade> -Use this when working with Arcade Physics in Phaser 4: enabling physics, velocity, acceleration, gravity, collisions (collide/overlap), world bounds, physics groups, static bodies, body properties, and collision categories. - -<quick_start> -Enable Arcade Physics in the game config, then create physics-enabled sprites via `this.physics.add`. - -```js -class GameScene extends Phaser.Scene { - create() { - // Physics sprite (dynamic body, affected by gravity) - this.player = this.physics.add.sprite(100, 300, 'player'); - this.player.setCollideWorldBounds(true); - this.player.setBounce(0.2); - - // Static group (immovable platforms) - this.platforms = this.physics.add.staticGroup(); - this.platforms.create(400, 568, 'ground').setScale(2).refreshBody(); - - // Register a persistent collider (checked every frame automatically) - this.physics.add.collider(this.player, this.platforms); - - this.cursors = this.input.keyboard.createCursorKeys(); - } - - update() { - if (this.cursors.left.isDown) { - this.player.setVelocityX(-160); - } else if (this.cursors.right.isDown) { - this.player.setVelocityX(160); - } else { - this.player.setVelocityX(0); - } - } -} - -// Enable Arcade Physics in game config -const config = { - type: Phaser.AUTO, - width: 800, - height: 600, - physics: { - default: 'arcade', - arcade: { - gravity: { y: 300 }, - debug: false - } - }, - scene: GameScene -}; - -const game = new Phaser.Game(config); -``` -</quick_start> - -<core_concepts> -The `Phaser.Physics.Arcade.World` (`this.physics.world`) manages all bodies. Key properties: `gravity` (Vector2 applied to all dynamic bodies each step — set via config or `world.gravity.y = 300`), `bounds` (Rectangle, defaults to canvas size), `bodies` / `staticBodies` (Sets of bodies), `colliders` (registered Collider objects), `fps` (steps/sec, default 60, change via `world.setFPS(120)`), `fixedStep` (true = fixed timestep, false = sync to render fps), `timeScale` (1.0 normal, 2.0 = half speed, 0.5 = double), `isPaused` (toggle via `world.pause()` / `world.resume()`), `useTree` (RTree spatial index, disable for 5000+ bodies), `drawDebug` (set via config `debug: true`). - -A dynamic `Body` is created automatically when you use `this.physics.add.sprite()` or `.image()`; access it via `gameObject.body`. Key body properties: `velocity` (px/sec), `acceleration` (px/sec^2), `drag` (deceleration, applied only when acceleration is zero), `gravity` (per-body, added to world gravity), `bounce` (rebound factor relative to 1), `friction` (default `(1, 0)`, velocity imparted by immovable body to riding body), `maxVelocity` (default `(10000, 10000)`) / `maxSpeed` (scalar cap, default -1 = none), `mass` (default 1, affects collision momentum), `immovable` (if true, never moved by collisions), `pushable` (if false, reflects velocity to colliding body), `moves` (if false, position not updated by physics), `enable` (false removes from simulation), `useDamping` (when true, drag is a 0-1 multiplier instead of linear), `collideWorldBounds`, and the event opt-ins `onWorldBounds` / `onCollide` / `onOverlap`. - -Body shape: `isCircle` (via `setCircle`), `width`/`height`, `offset` (Vector2), `center` (read-only midpoint). Collision state (read-only, reset each step): `touching` / `blocked` / `wasTouching` — each `{ none, up, down, left, right }`; `embedded` (both overlapping with zero velocity). Angular: `angularVelocity` (deg/sec), `angularAcceleration`, `angularDrag`, `maxAngular` (default 1000), `allowRotation` (default true). - -A `StaticBody` never moves and is unaffected by gravity or velocity; it uses an optimized RTree for fast spatial lookups. Created via `this.physics.add.staticSprite()`, `.staticImage()`, or `.staticGroup()`. After changing the parent Game Object's position or scale, call `body.reset()` or `gameObject.refreshBody()` to sync — static bodies do not auto-sync. They have `collisionCategory` and `collisionMask` like dynamic bodies but no velocity, acceleration, drag, or gravity. -</core_concepts> - -<common_patterns> -Enabling physics on existing Game Objects: -```js -this.physics.add.existing(mySprite); // dynamic body -this.physics.add.existing(mySprite, true); // static body - -// Or via the world directly -this.physics.world.enable(mySprite); // dynamic -this.physics.world.enable(mySprite, Phaser.Physics.Arcade.STATIC_BODY); -``` - -Creating physics sprites and images: -```js -const player = this.physics.add.sprite(100, 200, 'player'); // dynamic body -const coin = this.physics.add.image(300, 100, 'coin'); // dynamic body -const wall = this.physics.add.staticImage(400, 300, 'wall'); // static body -const platform = this.physics.add.staticSprite(400, 500, 'plat'); // static body - -// Standalone bodies (no Game Object) -const sensor = this.physics.add.body(200, 200, 32, 32); // dynamic Body -const zone = this.physics.add.staticBody(100, 100, 64, 64); // static Body -``` - -Velocity and gravity: -```js -player.setVelocity(200, -300); // x=200 px/s, y=-300 px/s (upward) -player.setVelocityX(200); -player.body.velocity.set(200, -300); // equivalent via Vector2 - -player.setAcceleration(100, 0); -player.setMaxVelocity(300, 600); - -player.body.gravity.y = 200; // per-body gravity (added to world gravity) -player.body.allowGravity = false; // exempt from world gravity - -player.setDrag(300); // linear deceleration (acceleration must be zero) -player.body.useDamping = true; -player.setDrag(0.05); // damping mode: keeps 5% velocity/sec - -player.setBounce(0.5); // both axes -player.setBounceY(1); // full vertical bounce -``` - -Collide and overlap — two approaches: persistent Colliders (checked every frame automatically, preferred) or one-shot checks (called manually in `update()`): -```js -// --- Persistent Colliders --- -const collider = this.physics.add.collider(player, platforms); -const overlap = this.physics.add.overlap(player, coins, collectCoin, null, this); - -function collectCoin (player, coin) { - coin.disableBody(true, true); // disable physics and hide -} - -collider.active = false; // temporarily disable -collider.destroy(); // remove permanently - -// With processCallback (must return boolean to allow collision) -this.physics.add.collider(player, enemies, onHit, canCollide, this); -function canCollide (player, enemy) { - return !player.getData('invincible'); -} - -// --- One-shot checks (in update) --- -this.physics.collide(player, platforms); -this.physics.overlap(player, coins, collectCoin, null, this); - -// Self-collision within a single group -this.physics.add.collider(enemies, enemies); -``` -`collide` performs separation (pushes bodies apart); `overlap` only detects intersection without moving anything — use `overlap` for triggers like pickups. - -Groups — dynamic group members get dynamic bodies automatically; static group members get static bodies: -```js -const bullets = this.physics.add.group({ - classType: Phaser.Physics.Arcade.Sprite, // default: ArcadeSprite - maxSize: 20, - collideWorldBounds: true, - bounceX: 1, - bounceY: 1, - velocityX: 200, - allowGravity: false, - immovable: false -}); - -const platforms = this.physics.add.staticGroup(); -platforms.create(400, 568, 'ground'); -platforms.create(600, 400, 'ground'); - -// After modifying a static group member's transform, refresh: -platforms.create(400, 568, 'ground').setScale(2).refreshBody(); - -// Group velocity helpers -bullets.setVelocity(200, 0); // all members -bullets.setVelocityX(200); -bullets.setVelocityY(0, 10); // with step increment per member -``` -PhysicsGroup config keys (applied as defaults to new members at creation only — changing them later does NOT retroactively update existing members): `collideWorldBounds`, `bounceX/Y`, `accelerationX/Y`, `dragX/Y`, `gravityX/Y`, `frictionX/Y`, `velocityX/Y`, `angularVelocity`, `angularAcceleration`, `angularDrag`, `maxVelocityX/Y`, `maxSpeed`, `mass`, `immovable`, `allowDrag`, `allowGravity`, `allowRotation`, `useDamping`, `enable`. - -World bounds: -```js -this.physics.world.setBounds(0, 0, 1600, 1200, true, true, false, true); -this.physics.world.setBoundsCollision(true, true, false, true); // which edges collide - -player.setCollideWorldBounds(true); -player.body.setBoundsRectangle(new Phaser.Geom.Rectangle(100, 100, 600, 400)); -player.body.worldBounce = new Phaser.Math.Vector2(0.5, 0.5); - -// Detect world bounds hit via event (requires opt-in) -player.body.onWorldBounds = true; -this.physics.world.on('worldbounds', (body, up, down, left, right) => { - console.log('Hit edge:', { up, down, left, right }); -}); -``` - -Body properties: -```js -player.body.setSize(24, 32, true); // width, height, re-center on GO -player.body.setOffset(4, 0); // offset from GO position -player.body.setCircle(16); // circular body, radius 16 -player.body.setCircle(16, 4, 4); // circular with offset - -player.body.setImmovable(true); // not moved by collisions -player.body.setPushable(false); // reflects velocity to collider -player.body.slideFactor.set(0, 0); // Sokoban-style: stops after push -player.body.setMass(2); // affects momentum exchange - -player.disableBody(true, true); // disable body + hide Game Object -player.enableBody(true, x, y, true, true); // re-enable at position + show - -player.body.checkCollision.up = false; // don't collide from above -player.body.syncBounds = true; // auto-sync size to texture -``` - -Collision categories filter which bodies can collide using bitmask values (max 32 categories): -```js -const CAT_PLAYER = this.physics.nextCategory(); // 0x0002 -const CAT_ENEMY = this.physics.nextCategory(); // 0x0004 -const CAT_BULLET = this.physics.nextCategory(); // 0x0008 - -player.setCollisionCategory(CAT_PLAYER); -enemy.setCollisionCategory(CAT_ENEMY); -bullet.setCollisionCategory(CAT_BULLET); - -player.setCollidesWith([CAT_ENEMY]); // player hits enemies only -enemy.setCollidesWith([CAT_PLAYER, CAT_BULLET]); // enemies hit player + bullets -bullet.setCollidesWith([CAT_ENEMY]); - -player.addCollidesWith(CAT_BULLET); -player.removeCollidesWith(CAT_ENEMY); -player.willCollideWith(CAT_ENEMY); // returns boolean -player.resetCollisionCategory(); // reset to default (collides with everything) - -// Works on Groups too -enemies.setCollisionCategory(CAT_ENEMY); -enemies.setCollidesWith([CAT_PLAYER, CAT_BULLET]); -``` -Default: all bodies have category `0x0001` and mask `1`, so everything collides because `(1 & 1) !== 0`. PhysicsGroup defaults to mask `2147483647` (all bits). After changing categories, call `resetCollisionCategory()` to set the mask back to all bits. -</common_patterns> - -<events> -All events are emitted on `this.physics.world` and require opt-in per body (set `body.onCollide`, `body.onOverlap`, or `body.onWorldBounds` to `true`): - -- `'collide'` (COLLIDE) — two bodies collide and at least one has `onCollide = true`. Args `(gameObject1, gameObject2, body1, body2)`. -- `'overlap'` (OVERLAP) — two bodies overlap and at least one has `onOverlap = true`. Args `(gameObject1, gameObject2, body1, body2)`. -- `'worldbounds'` (WORLD_BOUNDS) — body hits a world edge and has `onWorldBounds = true`. Args `(body, up, down, left, right)`. -- `'tilecollide'` / `'tileoverlap'` — body collides/overlaps a tile. Args `(gameObject, tile, body)`. -- `'worldstep'` — after each physics step. Args `(delta)`. -- `'pause'` / `'resume'` — world paused/resumed. No args. - -Useful scene-plugin helpers: `this.physics.accelerateTo`, `moveTo`, `velocityFromAngle`, `velocityFromRotation`, `closest`, `furthest`, `overlapRect`, `overlapCirc`. -</events> - -<gotchas> -1. `debug: true` in production — debug rendering is expensive. Always disable for release builds. -2. Static body transform changes — after changing position, scale, or origin of a static body's Game Object, you must call `body.reset()` or `gameObject.refreshBody()`. Static bodies do not auto-sync. -3. `collide` vs `overlap` — `collide` performs separation (pushes bodies apart); `overlap` only detects intersection. Use `overlap` for triggers like pickups. -4. Persistent Collider vs one-shot — `this.physics.add.collider()` is checked every frame automatically; `this.physics.collide()` is a one-shot check you must call each frame in `update()`. Prefer persistent Colliders. -5. Events require opt-in — world events fire only if the relevant body property (`onCollide`, `onOverlap`, `onWorldBounds`) is `true`. -6. Collision categories default — all bodies default to category `0x0001` / mask `1` (PhysicsGroup defaults to mask `2147483647`). Use `this.physics.nextCategory()` for new values; max 32 categories. After changing categories, call `resetCollisionCategory()` to set the mask to all bits. -7. Group defaults only apply at creation — changing a PhysicsGroup's `defaults` later does not retroactively update existing members. -8. `customUpdate: true` in the arcade config stops the world from auto-updating on the Scene UPDATE event — you must call `this.physics.world.update(time, delta)` manually. -9. `useDamping` drag values should be small (e.g. `0.05`) since they act as a multiplier — `0.05` means the body keeps 5% of its velocity per second. -10. Immovable vs pushable — `immovable = true` means never moved by collisions at all; `pushable = false` means it reflects velocity back to the colliding body but can still be separated. -11. Overlap with TilemapLayer — when using `overlapOnly` with a TilemapLayer, every tile is checked regardless of collision settings on individual tiles. -12. RTree threshold — the RTree spatial index (enabled by default) becomes expensive to rebuild with very large numbers of dynamic bodies. Consider `useTree: false` for 5000+ dynamic bodies. -</gotchas> -</physics_arcade> diff --git a/skills/base44-platform-games/references/phaser/physics_matter.md b/skills/base44-platform-games/references/phaser/physics_matter.md deleted file mode 100644 index 000c5c7..0000000 --- a/skills/base44-platform-games/references/phaser/physics_matter.md +++ /dev/null @@ -1,323 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: physics matter - -<physics_matter> -Use this when working with Matter.js physics in Phaser 4: rigid bodies, compound bodies, constraints (joints/springs), composites, sensors, collision filtering, pointer dragging, tilemap integration, and debug rendering. - -<quick_start> -Enable Matter physics in the game config, then create bodies via `this.matter.add`. - -```js -class GameScene extends Phaser.Scene { - create() { - // Matter sprite (dynamic, has animation support) - this.player = this.matter.add.sprite(400, 200, 'player'); - this.player.setBounce(0.5); - this.player.setFriction(0.05); - - // Matter image (dynamic, no animation) - const box = this.matter.add.image(300, 100, 'crate'); - - // Static body from raw shape - this.matter.add.rectangle(400, 580, 800, 40, { isStatic: true }); - - // Enable pointer dragging on all bodies - this.matter.add.mouseSpring(); - - this.cursors = this.input.keyboard.createCursorKeys(); - } - - update() { - if (this.cursors.left.isDown) { - this.player.setVelocityX(-5); - } else if (this.cursors.right.isDown) { - this.player.setVelocityX(5); - } - if (this.cursors.up.isDown && this.player.body.velocity.y > -0.1) { - this.player.setVelocityY(-10); - } - } -} - -// Enable Matter physics in game config -const config = { - type: Phaser.AUTO, - width: 800, - height: 600, - physics: { - default: 'matter', - matter: { - gravity: { y: 1 }, - enableSleeping: true, - debug: true, - setBounds: true // walls around canvas edges - } - }, - scene: GameScene -}; - -const game = new Phaser.Game(config); -``` -</quick_start> - -<core_concepts> -The `MatterPhysics` class is the scene-level plugin (`this.matter`). Key properties: `this.matter.add` (Factory for creating bodies, constraints, and Game Objects, auto-added to world), `this.matter.world` (the `World` instance managing the engine, bounds, and debug rendering; extends EventEmitter), `this.matter.body` / `bodies` / `composite` / `composites` / `constraint` (direct references to Matter.js modules for low-level use), and `this.matter.bodyBounds` (helper for aligning bodies by visual bounds). - -The `World` (`this.matter.world`) exposes `engine` (the `MatterJS.Engine`), `localWorld` (the `MatterJS.World` composite containing all bodies/constraints), `enabled` (false pauses simulation), `autoUpdate` (true = engine updates each game step), and `walls` (`{ left, right, top, bottom }` boundary wall bodies, or null). World config passed under `physics.matter`: `gravity` (default `{ x: 0, y: 1 }`, set `false` to disable), `setBounds` (`true` or `{ x, y, width, height, thickness, left, right, top, bottom }`), `enableSleeping` (allow bodies to sleep when at rest), `positionIterations` (default 6), `velocityIterations` (default 4), `constraintIterations` (default 2), `timing.timeScale` (1 normal, 0 freezes, 0.5 slow-motion), `autoUpdate` (default true), `debug` (`true` or a MatterDebugConfig object), `runner` (use `runner.fps` for fixed timestep). - -Phaser provides two physics-aware Game Object classes plus a function to add physics to any Game Object: `Phaser.Physics.Matter.Sprite` (extends Sprite with all Matter components, supports animations, created via `this.matter.add.sprite(x, y, key, frame, options)`); `Phaser.Physics.Matter.Image` (extends Image, no animation, lighter weight, via `this.matter.add.image(...)`); and `MatterGameObject(world, gameObject, options)` (injects all Matter components into any existing Game Object, via `this.matter.add.gameObject(mySprite, options)`). Both MatterSprite and MatterImage default to a rectangle body matching the texture size — pass `options.shape` to override. - -All Matter Game Objects have these component methods mixed in: Velocity (`setVelocity`, `setVelocityX/Y`, `getVelocity`, `setAngularVelocity`, `setAngularSpeed`, ...), Force (`applyForce`, `applyForceFrom`, `thrust`, `thrustLeft/Right/Back`), Bounce (`setBounce` — restitution 0-1), Friction (`setFriction(value, air?, fstatic?)`, `setFrictionAir`, `setFrictionStatic`), Mass (`setMass`, `setDensity`, `centerOfMass`), Gravity (`setIgnoreGravity`), Sensor (`setSensor`, `isSensor`), Static (`setStatic`, `isStatic`), Sleep (`setToSleep`, `setAwake`, `setSleepThreshold`, `setSleepEvents`), Collision (`setCollisionCategory`, `setCollisionGroup`, `setCollidesWith`, `setOnCollide`, `setOnCollideEnd`, `setOnCollideActive`, `setOnCollideWith`), and SetBody (`setRectangle`, `setCircle`, `setPolygon`, `setTrapezoid`, `setBody`, `setExistingBody`). -</core_concepts> - -<common_patterns> -Runtime setup and world configuration: -```js -this.matter.world.setGravity(0, 2); // x, y, scale (default 0.001) -this.matter.world.disableGravity(); -this.matter.world.setBounds(0, 0, 1600, 1200, 64, true, true, true, true); - -this.matter.set60Hz(); // fixed timestep -this.matter.world.autoUpdate = false; // then manual: this.matter.step(16.666); -this.matter.pause(); -this.matter.resume(); -``` - -Creating Matter sprites and images: -```js -const player = this.matter.add.sprite(200, 300, 'hero'); // default rect body matching texture - -// Custom body shapes via options.shape -const ball = this.matter.add.image(400, 100, 'ball', null, { shape: { type: 'circle', radius: 24 } }); -const hex = this.matter.add.sprite(300, 100, 'hex', null, { shape: { type: 'polygon', sides: 6, radius: 32 } }); -const ship = this.matter.add.sprite(400, 200, 'ship', null, { - shape: { type: 'fromVerts', verts: '0 0 40 0 40 40 20 60 0 40' } -}); - -// PhysicsEditor shape data -const shapes = this.cache.json.get('shapes'); -const enemy = this.matter.add.sprite(500, 200, 'enemy', null, { shape: shapes.enemy }); - -// Add Matter physics to an existing Game Object -const existingSprite = this.add.sprite(100, 100, 'box'); -this.matter.add.gameObject(existingSprite, { restitution: 0.8 }); -``` - -Body config options (`MatterBodyConfig`) — pass as the `options` parameter to any factory method or Matter Game Object: `label`, `isStatic`, `isSensor`, `angle` (radians), `timeScale`, `ignoreGravity`, `ignorePointer`, `density` (0.001 default, auto-calculates mass), `mass`, `restitution` (bounce 0-1), `friction` (0-1), `frictionAir`, `frictionStatic`, `slop` (overlap tolerance), `chamfer` (`{ radius: 5 }` for rounded corners), `collisionFilter: { category, mask, group }`, `onCollideCallback` / `onCollideEndCallback` / `onCollideActiveCallback`, and `shape` (for Game Objects). - -Velocity, forces, and thrust — note forces use very small values (`0.01`-`0.1`) and velocity is in units per step, not pixels/sec: -```js -sprite.setVelocity(3, -5); -sprite.setVelocityX(-3); -sprite.setAngularVelocity(0.05); -const vel = sprite.getVelocity(); // { x, y } - -sprite.applyForce({ x: 0.05, y: 0 }); -sprite.applyForceFrom(position, { x: 0.02, y: -0.02 }); - -sprite.thrust(0.05); // forward (relative to body angle) -sprite.thrustBack(0.05); -sprite.thrustLeft(0.03); -sprite.thrustRight(0.03); - -// Batch operations via this.matter -this.matter.setVelocity(arrayOfBodies, 2, -3); -this.matter.applyForce(arrayOfBodies, { x: 0.01, y: 0 }); -``` - -Constraints (joints and springs) — `constraint(bodyA, bodyB, length?, stiffness?, options?)`, aliases `joint` / `spring`: -```js -const rigid = this.matter.add.constraint(bodyA, bodyB, 100, 1); // rigid joint -const spring = this.matter.add.constraint(bodyA, bodyB, 200, 0.02, { damping: 0.05 }); -const pin = this.matter.add.constraint(bodyA, bodyB, 0, 0.9); // pin joint - -// World constraint (one body pinned to a world point) -this.matter.add.worldConstraint(body, 50, 0.5, { pointA: { x: 400, y: 100 } }); - -// Offset attachment points -this.matter.add.constraint(bodyA, bodyB, 80, 1, { - pointA: { x: 20, y: 0 }, // offset from bodyA center - pointB: { x: -20, y: 0 } // offset from bodyB center -}); - -this.matter.getConstraintLength(constraint); // distance between anchor points -this.matter.world.removeConstraint(constraint); -``` - -Composites (stacks, chains, soft bodies): -```js -const stack = this.matter.add.stack(100, 100, 5, 4, 10, 10, (x, y) => { - return this.matter.bodies.rectangle(x, y, 40, 40); -}); -const imageStack = this.matter.add.imageStack('crate', null, 100, 100, 5, 4, 5, 5); -this.matter.add.chain(stack, 0.5, 0, -0.5, 0, { stiffness: 0.7 }); -this.matter.add.mesh(stack, 5, 4, true, { stiffness: 0.5 }); // grid with constraints + cross braces -this.matter.add.softBody(200, 100, 5, 5, 0, 0, true, 10, { friction: 0.1 }, { stiffness: 0.5 }); - -// Pre-built composites -this.matter.add.newtonsCradle(300, 50, 5, 20, 200); -this.matter.add.car(400, 300, 120, 30, 25); -``` - -Compound bodies combine multiple shapes into one body — the first part is the parent. Parts share position, angle, and velocity, and constraints must target the parent, not the parts: -```js -const partA = this.matter.bodies.rectangle(0, 0, 60, 20); -const partB = this.matter.bodies.circle(0, -30, 15); -const compoundBody = this.matter.body.create({ parts: [partA, partB] }); - -const player = this.matter.add.sprite(400, 200, 'hero'); -player.setExistingBody(compoundBody); -``` - -Sleep system — bodies at rest can sleep to skip simulation (requires `enableSleeping: true`): -```js -if (body.isSleeping) { /* body is at rest */ } -sprite.setSleepThreshold(30); // lower = falls asleep faster (default 60) -sprite.setToSleep(); -sprite.setAwake(); -sprite.setSleepEvents(true, true); // enable sleepstart/sleepend events - -this.matter.world.on('sleepstart', (event, body) => {}); -this.matter.world.on('sleepend', (event, body) => {}); -``` - -Sensors detect collisions but do not physically react — useful for trigger zones and pickups. They fire normal collision events (use `collisionstart`/`end` to detect entry/exit): -```js -const trigger = this.matter.add.rectangle(400, 300, 100, 100, { isSensor: true, isStatic: true }); -sprite.setSensor(true); -sprite.isSensor(); -``` - -Collision categories and filtering use bitmasks: `category` (which group this body belongs to, power of 2), `mask` (which categories it collides with), and `group` (shortcut: same positive = always collide, same negative = never collide, 0 = use category/mask): -```js -const PLAYER = this.matter.world.nextCategory(); // 0x0002 (32 max) -const ENEMY = this.matter.world.nextCategory(); // 0x0004 -const GROUND = this.matter.world.nextCategory(); // 0x0008 - -player.setCollisionCategory(PLAYER); -player.setCollidesWith([ENEMY, GROUND]); - -bullet.setCollisionCategory(0x0010); -bullet.setCollidesWith([ENEMY, GROUND]); // bullets skip player - -// Collision groups: same negative = never collide with each other -const noCollide = this.matter.world.nextGroup(true); -spriteA.setCollisionGroup(noCollide); -spriteB.setCollisionGroup(noCollide); - -// Via body config: collisionFilter: { category, mask, group } -// Batch: this.matter.setCollisionCategory([bodyA, bodyB], ENEMY); -``` - -Collision callbacks — per-body callbacks on Matter Game Objects, plus Game Object-level events: -```js -player.setOnCollide((pair) => { /* pair.bodyA, pair.bodyB */ }); -player.setOnCollideEnd((pair) => {}); -player.setOnCollideActive((pair) => {}); -player.setOnCollideWith(enemy, (body, pair) => {}); - -player.on('collide', (bodyA, bodyB, pair) => {}); -player.on('collideEnd', (bodyA, bodyB, pair) => {}); -``` - -Tilemap integration — collision MUST be set first via `setCollisionByProperty` etc.: -```js -const map = this.make.tilemap({ key: 'level' }); -const tileset = map.addTilesetImage('tiles', 'tiles-img'); -const layer = map.createLayer('Ground', tileset, 0, 0); - -layer.setCollisionByProperty({ collides: true }); // MUST set collision first -this.matter.world.convertTilemapLayer(layer); // creates MatterTileBody per colliding tile - -// Uses Tiled collision shapes if defined, otherwise tile bounds. -// Access: tile.physics.matterBody -// Individual tile: this.matter.add.tileBody(tile, { isStatic: true, friction: 0.5 }); -// After map changes: this.matter.world.convertTiles([tile1, tile2]); -``` - -Friction types — Matter has three independent friction values: -```js -sprite.setFriction(0.1); // dynamic: resistance during motion (0-1) -sprite.setFrictionStatic(0.5); // static: resistance before motion starts -sprite.setFrictionAir(0.05); // air: environmental drag (default 0.01) -sprite.setFriction(0.1, 0.02, 0.3); // set all three: dynamic, air, static -``` - -Complex shapes from vertices (concave shapes auto-decomposed): -```js -const body = this.matter.add.fromVertices(400, 300, '0 0 40 0 40 40 20 60 0 40'); - -const vertexSets = [ - [{ x: 0, y: 0 }, { x: 40, y: 0 }, { x: 40, y: 40 }], - [{ x: 40, y: 40 }, { x: 20, y: 60 }, { x: 0, y: 40 }] -]; -this.matter.add.fromVertices(300, 200, vertexSets); -``` - -Pointer constraint (mouse/touch dragging): -```js -const pc = this.matter.add.mouseSpring({ stiffness: 0.2, damping: 0.1 }); -pc.active = false; // disable temporarily -body.ignorePointer = true; // prevent specific body from being dragged -pc.stopDrag(); // release current drag programmatically -pc.destroy(); - -this.matter.world.on('dragstart', (body, part, constraint) => {}); -this.matter.world.on('drag', (body, constraint) => {}); -this.matter.world.on('dragend', (body, constraint) => {}); -``` - -Queries (raycasting and hit testing): -```js -const hits = this.matter.intersectPoint(pointer.x, pointer.y); // bodies at point -const contains = this.matter.containsPoint(body, x, y); // point-in-body test -const inRegion = this.matter.intersectRect(100, 100, 200, 200); // bodies in rect -const rayHits = this.matter.intersectRay(0, 300, 800, 300, 1); // raycast -const colliding = this.matter.intersectBody(playerBody); // body overlap - -this.matter.overlap(playerBody, enemyBodies, (bodyA, bodyB, info) => { - console.log('Overlapping', bodyA, bodyB); -}); -``` - -Debug rendering — pass options as the `debug` property in the matter config. Boolean flags include `showBody`, `showStaticBody`, `showVelocity`, `showCollisions`, `showSensors`, `showJoint`, `showPositions`, `showBounds`, `showAxes`, `showAngleIndicator`, `showSleeping`, `showConvexHulls`, `showInternalEdges`, `renderFill`, `renderLine`; color/style options include `lineColor`, `lineThickness`, `fillColor`, `staticLineColor`, `staticFillColor`, `sensorLineColor`, `jointColor`, `pinColor`, `springColor`, `anchorColor`, `positionColor`: -```js -this.matter.world.drawDebug = false; // toggle at runtime -this.matter.world.debugGraphic.visible = false; - -this.matter.world.setBodyRenderStyle(body, 0xff0000, 1, 2, 0x00ff00, 0.5); -this.matter.world.setConstraintRenderStyle(constraint, 0xffff00, 1, 2); -``` -</common_patterns> - -<events> -All events are emitted on `this.matter.world` (which extends EventEmitter): - -- `'collisionstart'` `(event, bodyA, bodyB)` — two bodies first start colliding. -- `'collisionactive'` `(event, bodyA, bodyB)` — two bodies are still colliding. -- `'collisionend'` `(event, bodyA, bodyB)` — two bodies stop colliding. -- `'beforeupdate'` / `'afterupdate'` `(event)` — before/after each engine update step. -- `'beforeadd'` / `'afteradd'` / `'beforeremove'` / `'afterremove'` `(event)` — around body/constraint add/remove. -- `'dragstart'` `(body, part, constraint)`, `'drag'` `(body, constraint)`, `'dragend'` `(body, constraint)` — pointer dragging. -- `'sleepstart'` / `'sleepend'` `(event, body)` — body sleeps/wakes (requires `setSleepEvents`). -- `'pause'` / `'resume'` — world paused/resumed. No args. - -Collision events include `event.pairs` — an array of collision pair objects with `bodyA`, `bodyB`, collision depth, and normal. -</events> - -<gotchas> -- Force values are tiny. Use `0.01`-`0.1` for forces, `1`-`15` for velocity. Not pixel-based. -- `setBody`/`setRectangle`/etc. resets all properties — mass, friction, collision filters, callbacks are wiped. Re-apply after changing shape. -- Constraints must target the parent body, not compound body `parts`. -- 32 collision categories max. Each `nextCategory()` uses one bit. -- `collisionFilter.group` overrides category/mask. Same positive = always collide; same negative = never collide; zero/different = use category/mask. -- Sensors still need matching collision filters to fire events. -- Matter position is the center of mass, not top-left (unlike Arcade Physics). -- Sleep events require opt-in: `sprite.setSleepEvents(true, true)`. -- Tilemap conversion requires collision set first via `setCollisionByProperty` etc. -- Restitution uses `Math.max(bodyA.restitution, bodyB.restitution)` — the bouncier value wins. -- `body.ignorePointer = true` prevents the pointer constraint from dragging that body. -</gotchas> -</physics_matter> diff --git a/skills/base44-platform-games/references/phaser/render_textures.md b/skills/base44-platform-games/references/phaser/render_textures.md deleted file mode 100644 index 1e50e19..0000000 --- a/skills/base44-platform-games/references/phaser/render_textures.md +++ /dev/null @@ -1,245 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: render textures - -<render_textures> -Use this when drawing game objects to off-screen textures in Phaser 4: the RenderTexture game object, DynamicTexture for shared textures, the Stamp helper, command-buffer rendering, snapshots, procedural generation, and minimaps. - -<quick_start> -```js -// In a Scene's create() method: - -// 1. RenderTexture -- a visible game object with its own DynamicTexture -const rt = this.add.renderTexture(400, 300, 256, 256); -rt.draw('player', 128, 128); // draw a texture by key at center -rt.fill(0x222244, 0.5); // semi-transparent fill -rt.render(); // flush the command buffer - -// 2. DynamicTexture -- a shared texture in the Texture Manager -const dt = this.textures.addDynamicTexture('composite', 512, 512); -dt.stamp('coin', null, 64, 64, { scale: 2, angle: 45 }); -dt.render(); -this.add.image(400, 300, 'composite'); // any game object can use it - -// 3. Stamp game object -- lightweight Image that ignores camera scroll -const hud = this.add.stamp(10, 10, 'heart'); -``` -</quick_start> - -<core_concepts> -Phaser 4 splits texture-drawing into two layers. A **RenderTexture** is an Image game object with an auto-created DynamicTexture — created via `this.add.renderTexture(x, y, w, h)`, visible on its own, has position/scale/alpha (all Image components), but belongs to one Scene (no cross-scene use; share across objects via `saveTexture(key)`). A **DynamicTexture** is a texture in the Texture Manager — created via `this.textures.addDynamicTexture(key, w, h)`, not visible on its own (must be assigned to a game object), shared by key, and usable cross-scene since textures are global. - -Use **RenderTexture** when you need a single visible surface you draw onto (paint canvas, trail effect, composite sprite). Use **DynamicTexture** when many game objects share the same generated texture, you need a texture for masks/shaders, or you need cross-scene access. - -RenderTexture is a thin proxy — `draw()`, `stamp()`, `fill()`, `clear()`, `erase()`, `snapshot()` all delegate to its underlying `this.texture` (a DynamicTexture). It extends Image, so its origin defaults to (0.5, 0.5); call `rt.setOrigin(0, 0)` for top-left positioning (common for full-screen or minimap RTs). - -The command buffer (v4 architecture): drawing calls (`draw`, `stamp`, `fill`, `clear`, `erase`, `repeat`, `capture`) do NOT execute immediately — they push commands into a `commandBuffer` array. You must call `.render()` to flush and execute it: -```js -rt.clear(); -rt.fill(0x000000); -rt.draw(sprite, 128, 128); -rt.render(); // REQUIRED -- nothing appears without this -``` - -For RenderTexture game objects, the `renderMode` property controls automatic rendering: `'render'` (default) draws the texture contents to the frame each tick, you call `render()` manually when content changes; `'redraw'` calls `render()` automatically every frame but does NOT display itself (useful for textures reused by other objects); `'all'` calls `render()` every frame AND draws itself. Set via `rt.setRenderMode('all')` or `rt.setRenderMode('all', true)` (same, plus preserve the command buffer). - -Preserve mode: by default the command buffer clears after `render()`. Call `preserve(true)` to keep commands between renders so the same drawing replays each frame: -```js -rt.preserve(true); -rt.clear(); -rt.draw(sprite); -// On every subsequent render(), clear + draw will repeat -``` - -The `Stamp` game object (`this.add.stamp(x, y, texture, frame)`) is a lightweight Image subclass that ignores camera scroll and transform during rendering. It is used internally by DynamicTexture for drawing operations and is also useful for HUD elements. Note this is different from the `stamp()` method: `rt.stamp(key, frame, x, y, config)` draws a texture frame onto the surface with transform options, while `this.add.stamp(x, y, texture, frame)` creates a Stamp game object on the display list. -</core_concepts> - -<common_patterns> -Drawing sprites and game objects — `draw()` accepts renderable game objects, Groups, Containers, Display Lists, other RenderTextures/DynamicTextures, Texture Frames, texture key strings, or arrays of any of these: -```js -const rt = this.add.renderTexture(0, 0, 800, 600); - -rt.draw(sprite, 100, 100); // single object at offset -rt.draw([sprite1, sprite2, sprite3]); // array -rt.draw(enemyGroup); // group/container (only visible children drawn) -rt.draw(myContainer, 50, 50); // offset added to children positions -rt.draw(this.children); // entire Scene display list -rt.draw('explosion', 200, 200); // texture by string key -rt.render(); -``` -The `alpha` and `tint` parameters on `draw()` only apply to Texture Frames/strings — game objects use their own alpha and tint when drawn. - -Stamping textures with config — the frame is centered on x/y by default (origin 0.5): -```js -rt.stamp('bullet', null, 100, 100, { - alpha: 0.8, - tint: 0xff0000, - angle: 45, // degrees (takes precedence over rotation) - scale: 2, // uniform scale - scaleX: 1.5, // overrides scale for X - scaleY: 0.5, - originX: 0, // top-left origin - originY: 0, - blendMode: 0 -}); -rt.render(); -``` - -Capture — draws a game object with temporary property overrides, restoring originals afterward (useful for drawing at a different position/scale without modifying the object): -```js -rt.capture(player, { - x: 64, - y: 64, - scale: 0.5, - alpha: 0.8, - rotation: Math.PI / 4, - transform: 'world', // 'local', 'world', or a TransformMatrix - camera: someCamera // optional camera override -}); -rt.render(); -``` - -Filling and clearing: -```js -rt.fill(0xff0000); // solid red -rt.fill(0x000000, 0.5); // semi-transparent black -rt.fill(0x00ff00, 1, 10, 10, 100, 50); // green rect at (10,10) size 100x50 -rt.clear(); // clear everything (transparent) -rt.clear(10, 10, 100, 50); // clear a region -rt.render(); -``` - -Erasing uses ERASE blend mode to cut holes: -```js -rt.fill(0xffffff); // white background -rt.erase(circleSprite, 100, 100); // punch a hole shaped like the sprite -rt.render(); -``` - -Repeating / tiling a texture: -```js -rt.repeat('grass', null); // tile the whole surface -rt.repeat('brick', null, 0, 0, 256, 128); // tile a specific region -rt.repeat('tile', 'frame2', 0, 0, 512, 512, { - tileScaleX: 2, - tileScaleY: 2, - tilePositionX: 16, - alpha: 0.8 -}); -rt.render(); -``` - -Snapshots read pixel data from the framebuffer — they are expensive and blocking (use `readPixels`), so use sparingly: -```js -// Full texture snapshot -- callback receives an HTMLImageElement -rt.snapshot(function (image) { - document.body.appendChild(image); -}); - -// Area snapshot -rt.snapshotArea(0, 0, 128, 128, function (image) { - // image is 128x128 region from top-left -}, 'image/jpeg', 0.8); - -// Single pixel -- callback receives a Phaser.Display.Color (cheaper than full snapshot) -rt.snapshotPixel(64, 64, function (color) { - console.log(color.r, color.g, color.b, color.a); -}); -``` - -Using as a texture source for other objects — for a RenderTexture, register it via `saveTexture`; for a DynamicTexture, the key is set at creation: -```js -const rt = this.add.renderTexture(0, 0, 256, 256); -rt.draw(complexScene); -rt.render(); -rt.saveTexture('composited'); // register in the Texture Manager -this.add.image(400, 300, 'composited'); -this.add.sprite(100, 100, 'composited'); -// Updates to the RenderTexture automatically reflect on all users - -const dt = this.textures.addDynamicTexture('terrain', 1024, 1024); -dt.repeat('grass', null); -dt.render(); -this.add.image(512, 512, 'terrain'); // already keyed -``` - -Procedural generation: -```js -const dt = this.textures.addDynamicTexture('starfield', 800, 600); -dt.fill(0x000011); -for (let i = 0; i < 200; i++) { - const x = Phaser.Math.Between(0, 800); - const y = Phaser.Math.Between(0, 600); - const brightness = Phaser.Math.FloatBetween(0.3, 1); - dt.stamp('star', null, x, y, { alpha: brightness, scale: Phaser.Math.FloatBetween(0.5, 1.5) }); -} -dt.render(); -this.add.image(400, 300, 'starfield'); -``` - -Minimap pattern: -```js -create() { - this.minimap = this.add.renderTexture(700, 50, 150, 100); - this.minimap.setScrollFactor(0); // fixed to camera - this.minimap.setRenderMode('all'); // auto-render every frame - this.minimap.preserve(true); -} - -// In update or a callback: -this.minimap.clear(); -this.minimap.fill(0x111111); -this.minimap.capture(player, { x: px * 0.1, y: py * 0.1, scale: 0.1 }); -// render() is automatic in 'all' mode -``` - -Callbacks in the command buffer — insert logic between draw commands that executes during `render()`: -```js -rt.clear(); -rt.draw(background); -rt.callback(() => { sprite.setTint(0xff0000); }); // runs during render, after background -rt.draw(sprite); -rt.callback(() => { sprite.setTint(0xffffff); }); // restore after drawing -rt.render(); -``` - -Resizing erases all content and recreates the framebuffer: -```js -rt.resize(512, 512); -rt.resize(513, 513, false); // disable force-even rounding -``` - -Internal camera — both RenderTexture and DynamicTexture have a `.camera` property that controls how game objects are positioned when drawn. Scroll/zoom/rotate it to transform drawn content. Note `stamp()` ignores the camera; only `draw()` and game object rendering respect it: -```js -rt.camera.setScroll(100, 50); -rt.camera.setZoom(2); -rt.draw(sprite); // sprite drawn with camera transform applied -rt.render(); -``` - -Factory methods: -```js -this.add.renderTexture(x, y, width?, height?) // default 32x32 -this.add.stamp(x, y, texture, frame?) -this.textures.addDynamicTexture(key, width?, height?, forceEven?) // default 256x256 -``` -DynamicTexture also exposes `setSize(w, h?, forceEven?)`, `camera`, `commandBuffer`, `drawingContext` (WebGL DrawingContext with framebuffer), and `getWebGLTexture()`. The `stamp()` config supports `alpha`, `tint`, `angle` (degrees, overrides `rotation` if non-zero), `rotation` (radians), `scale`, `scaleX`/`scaleY`, `originX`/`originY` (default 0.5), `blendMode`. The `capture()` config supports `transform`, `camera`, `x`/`y`/`alpha`/`tint`/`angle`/`rotation`, `scale`/`scaleX`/`scaleY`, `originX`/`originY`, `blendMode`. -</common_patterns> - -<gotchas> -1. Must call `render()`. In Phaser 4 drawing methods buffer commands — nothing appears until `render()` is called (unless using `renderMode: 'all'` or `'redraw'`). -2. Cannot draw to itself. A DynamicTexture/RenderTexture silently skips entries that reference itself. -3. Command buffer clears after render. Call `preserve(true)` before drawing if you need the same commands to replay each frame. -4. `stamp()` ignores the internal camera. Only game objects drawn via `draw()` or `capture()` respect the `.camera` property; texture stamps are positioned absolutely. -5. Default sizes differ. RenderTexture defaults to 32x32; DynamicTexture defaults to 256x256. -6. `forceEven` rounds dimensions up. Width/height are rounded to even numbers for rendering quality by default — pass `false` as the last argument for exact odd dimensions. -7. Snapshots are expensive. They use `readPixels` (WebGL) which blocks the GPU pipeline. Avoid per-frame snapshots; `snapshotPixel` is cheaper than full `snapshot`. -8. WebGL1 has no anti-aliasing on framebuffers. Shapes and Graphics drawn to a RenderTexture may have jagged edges — use pre-rendered sprite textures instead. -9. `resize()` erases content. Resizing destroys and recreates the framebuffer. -10. `saveTexture` renames, does not copy. It assigns the DynamicTexture to the Texture Manager under that key; calling again with a different key renames it. Destroying the RenderTexture without saving first destroys the texture. -11. `draw()` alpha/tint params only affect Texture Frames. When drawing game objects, they use their own alpha and tint. -12. WebGL context loss. DynamicTexture contents are lost on context loss — listen for the `restorewebgl` event to redraw. -13. Groups/Containers: x, y offset is additive. With `rt.draw(group, x, y)`, the offset is added to each child's position — children at (100, 50) drawn with offset (10, 10) appear at (110, 60). -</gotchas> -</render_textures> diff --git a/skills/base44-platform-games/references/phaser/scale_and_responsive.md b/skills/base44-platform-games/references/phaser/scale_and_responsive.md deleted file mode 100644 index becf79d..0000000 --- a/skills/base44-platform-games/references/phaser/scale_and_responsive.md +++ /dev/null @@ -1,188 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: scale and responsive - -<scale_and_responsive> -Use this when making a Phaser 4 game responsive or handling display scaling: the ScaleManager, scale modes (FIT, RESIZE, EXPAND, ENVELOP), auto-center, zoom, fullscreen, orientation handling, and browser resize. - -<quick_start> -Scale-to-fit with centering — the most common setup for responsive games: - -```js -const config = { - type: Phaser.AUTO, - scale: { - parent: 'game-container', - mode: Phaser.Scale.FIT, - autoCenter: Phaser.Scale.CENTER_BOTH, - width: 800, - height: 600 - }, - scene: MyScene -}; - -const game = new Phaser.Game(config); -``` - -The `scale` config is parsed into a `Phaser.Core.Config` instance that the ScaleManager reads during boot. The ScaleManager sets the canvas element size and applies CSS scaling to fit it within its parent. -</quick_start> - -<core_concepts> -The ScaleManager is created during the Game boot sequence and is accessible at `game.scale` or `this.scale` from within a Scene. It extends EventEmitter. - -Three internal Size components drive all calculations: `gameSize` (`game.scale.gameSize`) is the unmodified game dimensions from config, used for world bounds and cameras (read via `game.scale.width` / `.height`); `baseSize` is the auto-rounded gameSize that sets the actual `canvas.width` / `canvas.height` attributes; `displaySize` is the CSS-scaled canvas size after applying scale mode, parent bounds, and zoom, setting `canvas.style.width` / `.height`. Scaling works by keeping the canvas element dimensions fixed (baseSize) and stretching it via CSS (displaySize) — equivalent to CSS `transform-scale` but without prefix issues. The `displayScale` property (Vector2) holds `baseSize / canvasBounds` and is used internally for input coordinate transformation. - -Scale modes (`Phaser.Scale.ScaleModes`, set via `scale.mode`; shorthand constants like `Phaser.Scale.FIT` also exist directly on `Phaser.Scale`): -- `NONE` (0) — no automatic scaling. Canvas uses config width/height; you manage sizing yourself. If you resize the canvas externally, call `game.scale.resize(w, h)` to update internals. -- `WIDTH_CONTROLS_HEIGHT` (1) — height adjusts automatically to maintain aspect ratio based on width. -- `HEIGHT_CONTROLS_WIDTH` (2) — width adjusts automatically to maintain aspect ratio based on height. -- `FIT` (3) — scales to fit inside parent while preserving aspect ratio. May leave empty space (letterbox/pillarbox). Most commonly used. -- `ENVELOP` (4) — scales to cover the entire parent while preserving aspect ratio. May extend beyond parent bounds (content gets cropped). -- `RESIZE` (5) — the canvas element itself is resized to fill parent. No CSS scaling, 1:1 pixel mapping; `gameSize`, `baseSize`, and `displaySize` all change to match parent. Beware GPU fill-rate on large displays. -- `EXPAND` (6) — hybrid of RESIZE and FIT: the visible area resizes to fill the parent, and the canvas scales to fit inside that area. Added in v3.80. - -Center modes (set via `scale.autoCenter`; centering uses CSS `marginLeft`/`marginTop` on the canvas): `NO_CENTER` (0, default), `CENTER_BOTH` (1), `CENTER_HORIZONTALLY` (2), `CENTER_VERTICALLY` (3). The parent element must have calculable bounds — if it has no defined width/height, centering will not work correctly. - -Zoom (set via `scale.zoom`, multiplies the display size): `NO_ZOOM` (1, default), `ZOOM_2X` (2, good for pixel art at low base resolution), `ZOOM_4X` (4), `MAX_ZOOM` (-1, automatically calculates the largest integer zoom that fits the parent). You can also pass any numeric value. Zoom affects CSS display size but not canvas resolution. - -Orientation (read via `game.scale.orientation`, string values): `Phaser.Scale.Orientation.LANDSCAPE` = `'landscape-primary'`, `LANDSCAPE_SECONDARY` = `'landscape-secondary'`, `PORTRAIT` = `'portrait-primary'`, `PORTRAIT_SECONDARY` = `'portrait-secondary'`. Convenience booleans: `isPortrait`, `isLandscape` (device orientation), `isGamePortrait`, `isGameLandscape` (game dimensions). -</core_concepts> - -<common_patterns> -FIT mode with centering (most common) — maintains aspect ratio and centers within the parent; empty space appears as letterbox/pillarbox bars (style the parent's background to control bar appearance): -```js -scale: { - parent: 'game-container', - mode: Phaser.Scale.FIT, - autoCenter: Phaser.Scale.CENTER_BOTH, - width: 1280, - height: 720 -} -``` - -Responsive resize (dynamic canvas size) — width/height accept percentage strings that resolve against the parent size (falling back to `window.innerWidth`/`innerHeight` if the parent has no size): -```js -scale: { - parent: 'game-container', - mode: Phaser.Scale.RESIZE, - width: '100%', - height: '100%' -} -``` -```js -// In your scene -- respond to size changes: -create() { - this.scale.on('resize', this.handleResize, this); -} - -handleResize(gameSize, baseSize, displaySize) { - this.cameras.resize(gameSize.width, gameSize.height); - // Reposition UI elements based on new dimensions -} -``` - -Fullscreen toggle — must be called from a `pointerup` gesture (not `pointerdown`): -```js -this.input.on('pointerup', () => { - if (this.scale.isFullscreen) { - this.scale.stopFullscreen(); - } else { - this.scale.startFullscreen(); - } -}); - -// Or the convenience method: -this.input.on('pointerup', () => { - this.scale.toggleFullscreen(); -}); -``` -`startFullscreen(fullscreenOptions)` requests browser fullscreen (default options `{ navigationUI: 'hide' }`), `stopFullscreen()` exits, `toggleFullscreen(fullscreenOptions)` toggles, `isFullscreen` is the read-only state. If no `fullscreenTarget` is configured, Phaser creates a temporary `<div>`, moves the canvas into it, and sends that div fullscreen (the div is removed when leaving). For iframes, the `allowfullscreen` attribute is required. - -Mobile orientation handling: -```js -create() { - this.scale.on('orientationchange', (orientation) => { - if (orientation === Phaser.Scale.LANDSCAPE) { - // Show game UI - } else { - // Show "rotate device" message - } - }); - - // Lock orientation (mobile browsers only, limited support): - this.scale.lockOrientation('landscape'); -} -``` - -Fixed size (no scaling) — in NONE mode, if you change the canvas size externally you must call `game.scale.resize(newWidth, newHeight)` to update all internal components including input coordinates: -```js -scale: { - mode: Phaser.Scale.NONE, - width: 800, - height: 600 -} -``` - -Pixel art with max zoom — `MAX_ZOOM` calculates the largest integer multiplier that fits in the parent; combined with `pixelArt: true` (which disables anti-aliasing) this gives crisp pixel rendering: -```js -scale: { - mode: Phaser.Scale.FIT, - autoCenter: Phaser.Scale.CENTER_BOTH, - width: 320, - height: 240, - zoom: Phaser.Scale.MAX_ZOOM -}, -pixelArt: true -``` - -Snap values for grid alignment — when the browser resizes, dimensions snap down to the nearest grid multiple (floor). Best with FIT mode. Can also be set at runtime via `game.scale.setSnap(16, 16)` or reset with `game.scale.setSnap()`: -```js -scale: { - mode: Phaser.Scale.FIT, - width: 800, - height: 600, - snap: { width: 16, height: 16 } -} -``` - -Min/max size constraints — the display size is clamped to these bounds during scaling calculations: -```js -scale: { - mode: Phaser.Scale.FIT, - width: 800, - height: 600, - min: { width: 400, height: 300 }, - max: { width: 1600, height: 1200 } -} -``` - -Other `scale` config properties: `expandParent` (default true, allows the ScaleManager to set parent/body CSS height to 100%), `parent` (DOM element or its ID; `undefined` = `document.body`, `null` = no parent so you manage canvas placement), `autoRound` (default false, floors display/style sizes for low-powered devices), `resizeInterval` (default 500ms, between parent size checks for fallback polling), `fullscreenTarget` (element to send fullscreen; if unset, Phaser creates a wrapper div). - -Useful runtime methods: `resize(width, height)` (NONE mode only — sets game/base/display sizes directly and updates the canvas + CSS), `setGameSize(width, height)` (scaled modes — changes base game size and re-applies scaling), `setParentSize(width, height)`, `setZoom(value)`, `setMaxZoom()`, `getMaxZoom()`, `setSnap(w, h)`, `getViewPort(camera?, out?)`, `refresh(prevW?, prevH?)` (forces recalculation and emits RESIZE), `transformX(pageX)` / `transformY(pageY)` (DOM-to-game coordinate), `getParentBounds()`, `updateCenter()`, `updateBounds()`. -</common_patterns> - -<events> -All events are on `Phaser.Scale.Events` and are emitted by the ScaleManager (`game.scale`): - -- `'resize'` (RESIZE) — any resize, refresh, or scale change. Args `(gameSize, baseSize, displaySize, previousWidth, previousHeight)`; the three Size params have `.width`, `.height`, `.aspectRatio`. -- `'orientationchange'` (ORIENTATION_CHANGE) — device orientation changes. Args `(orientation)`, a string constant from `Phaser.Scale.Orientation`. -- `'enterfullscreen'` (ENTER_FULLSCREEN) — browser successfully enters fullscreen. -- `'leavefullscreen'` (LEAVE_FULLSCREEN) — browser leaves fullscreen (via code or user ESC). -- `'fullscreenfailed'` (FULLSCREEN_FAILED) — fullscreen request denied. Args `(error)`. -- `'fullscreenunsupported'` (FULLSCREEN_UNSUPPORTED) — browser lacks the Fullscreen API. -</events> - -<gotchas> -1. Parent element must have dimensions. The ScaleManager relies on the parent's computed CSS size. An unstyled `<div>` has zero height, which breaks centering and scaling. Give it explicit CSS dimensions or rely on `expandParent: true` (default) to set body/parent to 100% height. -2. No padding on the parent. The ScaleManager does not account for parent padding. Apply padding to a wrapper element instead, or use margins on the parent's parent. -3. Do not style the canvas directly. The ScaleManager controls `canvas.style.width`, `.height`, `marginLeft`, and `marginTop`. External CSS on the canvas will conflict. -4. Fullscreen requires `pointerup`, not `pointerdown`. On touch devices, `pointerdown` fullscreen requests are blocked unless the document already received touch input. Always use `pointerup`. -5. Fullscreen in iframes needs `allowfullscreen`. Without this attribute on the iframe, fullscreen requests silently fail. -6. RESIZE mode and GPU fill-rate. RESIZE creates a 1:1 pixel canvas matching the parent; on high-resolution displays this can be very large. Monitor performance on low-end devices. -7. `resize()` vs `setGameSize()`. Use `resize()` only in NONE mode (it sets everything directly); use `setGameSize()` with FIT/ENVELOP/etc. (it preserves the scale mode calculations). -8. iOS height quirks. When the parent is the window on iOS, the ScaleManager uses `GetInnerHeight` to work around Safari's dynamic toolbar affecting `getBoundingClientRect`. -9. Percentage strings require a sized parent. `width: '100%'` / `height: '100%'` resolve against `parentSize`; if the parent has no size, they fall back to `window.innerWidth`/`innerHeight`. -10. `resizeInterval` is a fallback poll. Modern browsers dispatch `resize` events, but the ScaleManager also polls every `resizeInterval` ms (default 500) to catch edge cases on older browsers. -</gotchas> -</scale_and_responsive> diff --git a/skills/base44-platform-games/references/phaser/scenes.md b/skills/base44-platform-games/references/phaser/scenes.md deleted file mode 100644 index cdf82a0..0000000 --- a/skills/base44-platform-games/references/phaser/scenes.md +++ /dev/null @@ -1,274 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: scenes - -<scenes> -Use this when working with Phaser 4 scenes: scene lifecycle methods, transitions, parallel scenes, scene communication, sleeping, pausing, restarting, and the SceneManager. - -<quick_start> -Each Scene has its own lifecycle (init, preload, create, update), its own injected systems (`this.add`, `this.input`, `this.cameras`, etc.), and can be started, stopped, paused, slept, or run in parallel with other Scenes. The ScenePlugin (`this.scene`) controls all multi-scene orchestration. - -```js -// Minimal scene with all lifecycle methods -class GameScene extends Phaser.Scene { - constructor() { - super('GameScene'); - } - - init(data) { - // Called first. Receives data passed via scene.start('GameScene', { level: 1 }) - this.level = data.level || 1; - } - - preload() { - // Called after init. Load assets here. - this.load.image('logo', 'assets/logo.png'); - } - - create(data) { - // Called after preload completes. Set up game objects. - // 'data' is the same object passed to init. - this.add.image(400, 300, 'logo'); - } - - update(time, delta) { - // Called every frame while scene is RUNNING. - // time: current time (ms), delta: ms since last frame (smoothed) - } -} - -const config = { - width: 800, - height: 600, - scene: [GameScene] -}; - -const game = new Phaser.Game(config); -``` -</quick_start> - -<core_concepts> -Scene lifecycle states (constants on `Phaser.Scenes`): PENDING (0, registered but not started) -> INIT (1, `scene.init(data)`) -> START (2, `Systems.start()`, emits `start` and `ready`) -> LOADING (3, `scene.preload()` + loader runs) -> CREATING (4, `scene.create(data)`, same `data` as init) -> RUNNING (5, `scene.update(time, delta)` each frame, renders) -> PAUSED (6, no update but still renders) -> SLEEPING (7, no update, no render, state preserved) -> SHUTDOWN (8, emits `shutdown`, can restart) -> DESTROYED (9, fully destroyed, cannot restart). - -Flow with no preload: `init()` -> `create()` -> `update()` loop. Flow with preload: `init()` -> `preload()` -> loader runs -> `create()` -> `update()` loop. - -Scene-injected properties come via the InjectionMap. Global managers (shared across all scenes): `this.game`, `this.renderer`, `this.anims`, `this.cache`, `this.plugins`, `this.registry` (shared data manager), `this.scale`, `this.sound`, `this.textures`. Scene-specific systems (unique per scene): `this.sys` (never overwrite), `this.events` (scene event emitter), `this.cameras`, `this.add` (factory: creates and adds to display list), `this.make` (creator: creates but does NOT add), `this.scene` (ScenePlugin), `this.children` (display list), `this.lights`, `this.data` (scene data manager), `this.input`, `this.load`, `this.time`, `this.tweens`, `this.physics` (if configured), `this.matter` (if configured). - -Rename injected properties via scene config: -```js -const config = { - key: 'MyScene', - map: { - add: 'makeStuff', // this.makeStuff instead of this.add - load: 'loader' // this.loader instead of this.load - } -}; -``` - -The `SceneManager` is a game-level system. Do not call its methods directly -- use `this.scene` (the ScenePlugin) instead. It maintains an ordered array of scenes (determines render/update order) and processes a queue of operations at the start of each game step. All ScenePlugin methods are queued, not immediate -- they execute next frame. -</core_concepts> - -<common_patterns> -Switching between scenes: -```js -// start() -- shuts down current scene, starts target (target gets full lifecycle) -this.scene.start('LevelTwo', { score: 100 }); - -// restart() -- shuts down and restarts the same scene -this.scene.restart({ score: 0 }); - -// switch() -- sleeps current scene, starts/wakes target (current state preserved) -this.scene.switch('PauseMenu', { fromScene: 'GameScene' }); - -// transition() -- animated transition with duration -this.scene.transition({ - target: 'LevelTwo', - duration: 1000, - moveAbove: true, // render target above this scene - sleep: false, // false = stop this scene (default), true = sleep it - remove: false, // true = remove this scene from manager after transition - allowInput: false, // allow input on this scene during transition - data: { score: 100 }, - onUpdate: function (progress) { /* progress: 0 to 1 over duration */ } -}); -``` - -Running scenes in parallel: -```js -// launch() -- starts another scene in parallel (does NOT stop current scene) -this.scene.launch('UIScene', { lives: 3 }); - -// run() -- smart launcher: starts if not running, resumes if paused, wakes if sleeping -this.scene.run('UIScene', { lives: 3 }); - -// Control render order of parallel scenes -this.scene.bringToTop('UIScene'); // render last (on top) -this.scene.sendToBack('Background'); // render first (behind) -this.scene.moveAbove('GameScene', 'UIScene'); // UIScene renders above GameScene -this.scene.moveBelow('GameScene', 'Background'); -this.scene.moveUp('UIScene'); -this.scene.moveDown('UIScene'); -this.scene.swapPosition('SceneA', 'SceneB'); -``` - -Passing data between scenes: -```js -// Method 1: Pass data via start/launch/restart/switch/wake/run -- available in both init(data) and create(data) -this.scene.start('LevelScene', { level: 5, score: 1200 }); - -// Method 2: Access data later via sys.getData() -const data = this.sys.getData(); // returns settings.data - -// Method 3: Global registry (shared across ALL scenes) -this.registry.set('playerHP', 100); -const hp = this.registry.get('playerHP'); // 100, from any scene - -// Method 4: Scene-specific data manager -this.data.set('localValue', 42); -this.data.get('localValue'); // 42 - -// Method 5: Direct scene reference -const otherScene = this.scene.get('OtherScene'); -otherScene.somePublicProperty; - -// Method 6: Events on the global registry -this.registry.events.on('changedata-playerHP', (parent, value, previousValue) => { /* react */ }); -this.registry.set('playerHP', 50); // triggers the event in any listening scene -``` - -Pausing, sleeping, stopping: -```js -this.scene.pause(); // stops update loop, still renders -this.scene.resume('OtherScene', { message: 'welcome back' }); -this.scene.sleep(); // no update AND no render, state preserved -this.scene.wake('OtherScene', { data: 'here' }); -this.scene.stop(); // full shutdown, clears display list and timers - -// State checks -this.scene.isActive('OtherScene'); -this.scene.isPaused('OtherScene'); -this.scene.isSleeping('OtherScene'); -this.scene.isVisible('OtherScene'); - -// Control visibility/activity independently -this.scene.setActive(false); // pause -this.scene.setActive(true); // resume -this.scene.setVisible(false); // hide but still update -this.scene.setVisible(true); -``` - -Adding and removing scenes at runtime: -```js -// add(key, sceneConfig, autoStart, data) -this.scene.add('BonusLevel', BonusLevelScene, false, { someData: true }); -this.scene.remove('BonusLevel'); // destroyed, cannot be restarted - -// Spawn multiple instances from one class -for (let i = 0; i < 5; i++) { - this.scene.add('Level' + i, new LevelScene('Level' + i), false); -} -``` - -Cross-scene event communication -- one scene emits, another listens, decoupled. (In Base44 games, score/HUD readouts belong in the React overlay via EventBus — see the screen_space_ui rules; use this pattern when a second Phaser scene genuinely needs to react to gameplay, e.g. a background or effects layer. The UIScene below illustrates the mechanics.) -```js -// GameScene emits events -class GameScene extends Phaser.Scene { - collectCoin(coin) { - coin.destroy(); - this.events.emit('addScore', 10); - } -} - -// UIScene listens (launched in parallel with { active: true }) -class UIScene extends Phaser.Scene { - constructor() { - super({ key: 'UIScene', active: true }); - } - - create() { - this.score = 0; - this.scoreText = this.add.text(10, 10, 'Score: 0'); - const gameScene = this.scene.get('GameScene'); - gameScene.events.on('addScore', (points) => { - this.score += points; - this.scoreText.setText('Score: ' + this.score); - }); - } -} -``` - -Configuring plugins per scene: -```js -super({ key: 'MinimalScene', plugins: [] }); // disable all default plugins (no load/tweens/time/input/data/lights) -super({ key: 'PreloadScene', plugins: ['Loader'] }); // only this.load available -``` - -Scene config in constructor -- per-scene physics, loader path, or files preloaded before `preload()`: -```js -class Level1 extends Phaser.Scene { - constructor() { - super({ - key: 'Level1', - physics: { arcade: { debug: true, gravity: { y: 200 } } }, - loader: { path: 'assets/levels/1/' }, - // 'pack' loads files before preload() runs -- good for progress bar assets - pack: { files: [{ type: 'image', key: 'bar', url: 'loaderBar.png' }] } - }); - } -} -``` - -Safely restarting scenes -- reset state in `init()`, not the constructor. The constructor runs once; `init()` runs every start: -```js -class GameScene extends Phaser.Scene { - constructor() { - super('GameScene'); - // BAD: this.gameOver = false; -- only set once, not on restart - } - - init() { - // GOOD: reset state every time the scene starts - this.gameOver = false; - this.score = 0; - } - - create() { - // Clean up on shutdown to avoid stale references - this.events.once('shutdown', () => { - this.enemies = []; - }); - } -} -``` -</common_patterns> - -<events> -All events are emitted on `this.events` (the scene-specific EventEmitter). Listen via `this.events.on('eventname', callback)`. - -Lifecycle events: `'boot'` (sys; once, when scene first instantiated), `'start'` (sys; systems start), `'ready'` (sys, data; after start, for user code), `'create'` (scene; after `create()` runs, scene now RUNNING), `'preupdate'` / `'update'` / `'postupdate'` (time, delta; each frame), `'prerender'` / `'render'` (renderer; around scene render). - -State-change events: `'pause'` (sys, data), `'resume'` (sys, data), `'sleep'` (sys, data), `'wake'` (sys, data), `'shutdown'` (sys, data), `'destroy'` (sys). - -Transition events: `'transitionout'` (targetScene, duration; on source scene), `'transitioninit'` (fromScene, duration; on target during init), `'transitionstart'` (fromScene, duration; on target after create), `'transitionwake'` (fromScene, duration; on target if woken from sleep), `'transitioncomplete'` (scene; on target when done). - -Game object events: `'addedtoscene'` (gameObject, scene), `'removedfromscene'` (gameObject, scene). -</events> - -<gotchas> -- **Operations are queued, not immediate.** `this.scene.start('X')` happens at the next SceneManager update, not synchronously. Do not rely on the target scene's state within the same frame. -- **`start()` shuts down the calling scene.** It stops the current scene and starts X. To keep both running, use `launch()` or `run()`. -- **`switch()` sleeps, `start()` shuts down.** `switch()` preserves the current scene in memory (sleep); `start()` triggers a full shutdown. Sleeping scenes keep their events and references live. -- **`switch()` restarts a paused scene, never resumes it.** If you need resume behavior, use `run()` instead. -- **Paused scenes still render.** `pause()` only stops the update loop. Use `sleep()` to stop both update and render. -- **Do not overwrite `this.sys`.** It will break everything. -- **`this.scene.start()` with no key restarts the current scene** -- equivalent to `this.scene.restart()`. -- **Sleeping scenes still receive events from other scenes.** If Scene A is sleeping but Scene B emits on the global registry, Scene A's listeners still fire. Be careful with active listeners on sleeping scenes. -- **Scene render order = array order.** Scenes later in the array render on top. Use `bringToTop()`, `sendToBack()`, `moveAbove()`, `moveBelow()` to control layering. Scenes update in reverse order (top updates first, gets input priority) but render in forward order (appears on top). -- **`shutdown` vs `destroy`.** Shutdown puts a scene into hibernation (can restart); destroy permanently removes it. Listen to `'shutdown'` to free resources recreated on restart, `'destroy'` for final cleanup. -- **`this.physics` and `this.matter` are only available if the physics system is configured** -- otherwise undefined. -- **The `create` event fires AFTER the `create()` method returns**, and after the status changes to RUNNING. Listen for it if you need post-create setup. -- **Reset state in `init()`, not the constructor.** The constructor runs once; `init()` runs every start/restart. -- **Clean up on `shutdown`** via `this.events.once('shutdown', ...)` to clear arrays holding game objects and remove external listeners. Stale references to destroyed objects cause errors on restart. -</gotchas> -</scenes> diff --git a/skills/base44-platform-games/references/phaser/sprites_and_images.md b/skills/base44-platform-games/references/phaser/sprites_and_images.md deleted file mode 100644 index 85fbb75..0000000 --- a/skills/base44-platform-games/references/phaser/sprites_and_images.md +++ /dev/null @@ -1,207 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: sprites and images - -<sprites_and_images> -Use this when creating or manipulating Sprite and Image game objects in Phaser 4: factory methods, texture/frame selection, the component mixin system, and visual operations (position, scale, rotation, tint, flip, alpha, origin, depth). - -<quick_start> -```js -// In a Scene's create() method: - -// Static image (no animation support, slightly cheaper) -const bg = this.add.image(400, 300, 'background'); - -// Sprite (supports animations) -const player = this.add.sprite(100, 200, 'player', 'idle-0'); - -// Common operations -- all methods return `this` for chaining -player.setPosition(200, 300); -player.setScale(2); -player.setAngle(45); -player.setTint(0xff0000); -player.setAlpha(0.8); -player.setOrigin(0, 1); // bottom-left -player.setDepth(10); -player.setFlip(true, false); // flip horizontally -player.setVisible(false); - -// Chained -this.add.sprite(100, 100, 'coin') - .setScale(0.5) - .setTint(0xffff00) - .play('spin'); -``` -</quick_start> - -<core_concepts> -**Sprite vs Image.** Both extend `GameObject` and share the same set of component mixins. The only difference: Sprite includes an `AnimationState` instance (`sprite.anims`) and animation methods (`play`, `stop`, `chain`). Image has no animation support, no per-frame `preUpdate`, and is not added to the Scene `updateList`. Both support static texture display, tint/alpha/flip/scale/rotate, physics bodies, and input/hit areas. - -Rule of thumb: use `Image` for anything that does not need frame-by-frame animation -- it skips the per-frame `preUpdate` cost and has a smaller API surface. Use `Sprite` only when you need the Animation component. - -**The component mixin system.** Phaser builds Game Object classes by mixing component objects into the prototype. Both Sprite and Image share an identical Mixins array: Alpha, BlendMode, Depth, Flip, GetBounds, Lighting, Mask, Origin, RenderNodes, ScrollFactor, Size, TextureCrop, Tint, Transform, Visible, plus a render-specific mixin (SpriteRender / ImageRender). The base `GameObject` itself mixes in Filters and RenderSteps. When you see a method like `setAlpha()` on a Sprite, it comes from `Components.Alpha`, not the Sprite class itself. - -**Texture and frame** come from the `TextureCrop` component: `texture` (the Texture instance), `frame` (current Frame instance), `setTexture(key, frame)`, `setFrame(frame, updateSize, updateOrigin)`, `setCrop(x, y, width, height)`, and `isCropped`. The `texture` param accepts a string key or a `Phaser.Textures.Texture` instance. The `frame` param accepts a string name or numeric index; if omitted, the base frame (`'__BASE'`) is used. - -```js -// Change texture at runtime -sprite.setTexture('enemies', 'goblin-walk-1'); - -// Change only the frame (must belong to current texture) -sprite.setFrame('goblin-walk-2'); - -// setFrame(frame, updateSize=true, updateOrigin=true) -- pass false to skip resize/origin recalc -sprite.setFrame('small-frame', false, false); - -// Crop to a 50x50 region starting at (10, 10); call setCrop() with no args to reset -sprite.setCrop(10, 10, 50, 50); -sprite.setCrop(); -``` -</core_concepts> - -<common_patterns> -Position and size (Transform + Size components): -```js -sprite.x, sprite.y // position (read/write; default 0) -sprite.z // z position (does NOT control render order -- use depth) -sprite.setPosition(x, y, z, w) // y defaults to x if omitted -sprite.setRandomPosition(x, y, w, h) // random within area; defaults to game size -sprite.copyPosition(source) // copy from any {x, y} object - -sprite.width, sprite.height // native (un-scaled) size -sprite.displayWidth, sprite.displayHeight // scaled size (read/write -- setting adjusts scale) -sprite.setSize(width, height) // internal size (not visual) -sprite.setDisplaySize(width, height) // visual size (adjusts scale) -sprite.setSizeToFrame(frame) // reset size to match frame -``` - -Scaling and rotation (Transform): -```js -sprite.scale // uniform scale (getter returns average of scaleX+scaleY) -sprite.scaleX, sprite.scaleY // default 1 -sprite.setScale(x, y) // y defaults to x if omitted - -sprite.rotation // radians (clockwise: 0=right, PI/2=down) -sprite.angle // degrees (0=right, 90=down, 180/-180=left, -90=up) -sprite.setRotation(radians) -sprite.setAngle(degrees) -``` - -Tinting (Tint component, WebGL only): -```js -sprite.tint // getter returns tintTopLeft -sprite.tintTopLeft / tintTopRight / tintBottomLeft / tintBottomRight // default 0xffffff -sprite.tintMode // default Phaser.TintModes.MULTIPLY -sprite.isTinted // read-only - -sprite.setTint(topLeft, topRight, bottomLeft, bottomRight) // single arg applies to all four corners -sprite.setTintMode(mode) // MULTIPLY | FILL | ADD | SCREEN | OVERLAY | HARD_LIGHT -sprite.clearTint() // resets to 0xffffff + MULTIPLY -``` -Phaser 4 change: `setTintFill()` is removed -- use `setTint(color).setTintMode(Phaser.TintModes.FILL)`. - -Alpha (Alpha component): -```js -sprite.alpha // global alpha 0-1 (default 1) -sprite.alphaTopLeft / alphaTopRight / alphaBottomLeft / alphaBottomRight // per-corner (WebGL only) -sprite.setAlpha(topLeft, topRight, bottomLeft, bottomRight) // single arg applies to whole object -sprite.clearAlpha() // resets to 1 (fully opaque) -``` -Setting `alpha` to 0 clears the render flag so the object is not drawn; any non-zero value restores it. - -Flipping (Flip component) -- a rendering toggle only; does not affect physics bodies, hit areas, or scale value: -```js -sprite.flipX, sprite.flipY // boolean (default false) -sprite.setFlipX(value) -sprite.setFlipY(value) -sprite.setFlip(x, y) -sprite.toggleFlipX() -sprite.toggleFlipY() -sprite.resetFlip() // both to false -``` - -Origin and depth (Origin + Depth components): -```js -sprite.originX, sprite.originY // normalized 0-1 (default 0.5, center) -sprite.displayOriginX, sprite.displayOriginY // pixel value (originX * width) -sprite.setOrigin(x, y) // y defaults to x; defaults to 0.5 -sprite.setDisplayOrigin(x, y) // set origin in pixels -sprite.setOriginFromFrame() // use Frame pivot if set, else 0.5 - -sprite.depth // default 0 (higher = renders on top) -sprite.setDepth(value) // queues a depth sort -sprite.setToTop() // move to top of display list (no depth change) -sprite.setToBack() -sprite.setAbove(gameObject) -sprite.setBelow(gameObject) -``` -`setToTop`/`setToBack`/`setAbove`/`setBelow` change display list position without modifying the `depth` value. - -Additional components: -```js -sprite.visible, sprite.setVisible(value) // Visible -- invisible objects skip render but still update -sprite.scrollFactorX, sprite.scrollFactorY // ScrollFactor (default 1: moves with camera) -sprite.setScrollFactor(x, y) // 0 = fixed to camera; y defaults to x -sprite.blendMode, sprite.setBlendMode(value) // BlendMode (NORMAL, ADD, MULTIPLY, SCREEN, ERASE) -``` -Performance: changing blend mode breaks WebGL batches. Group objects by blend mode when possible to minimize draw calls. - -Destroying: -```js -sprite.destroy(); // removes from display + update lists, cleans up -sprite.ignoreDestroy = true; // prevent destruction (you manage lifecycle) -sprite.isDestroyed; // boolean, true after destroy() -``` -For Sprites, `destroy()` also calls `this.anims.destroy()` to clean up the AnimationState. -</common_patterns> - -<factory_methods> -```js -// this.add.sprite(x, y, texture, frame) -> Phaser.GameObjects.Sprite -this.add.sprite(x, y, texture, frame); - -// this.add.image(x, y, texture, frame) -> Phaser.GameObjects.Image -this.add.image(x, y, texture, frame); -``` -`x`, `y` are world coordinates (required). `texture` is a string key or Texture instance (required). `frame` is a string name or numeric index (optional). Both constructors run the same init sequence: `setTexture` -> `setPosition` -> `setSizeToFrame` -> `setOriginFromFrame` -> `initRenderNodes`. The factory does `this.displayList.add(new Sprite/Image(...))`. -</factory_methods> - -<specialized_game_objects> -**NineSlice** -- scalable UI panels with fixed-size corners and stretchable edges/center. Ideal for buttons, panels, dialog boxes that resize without distorting borders: -```js -// nineslice(x, y, texture, frame, width, height, leftWidth, rightWidth, topHeight, bottomHeight) -const panel = this.add.nineslice(400, 300, 'panel', null, 300, 200, 20, 20, 20, 20); -panel.setSize(500, 400); // corners stay fixed, edges stretch -``` - -**TileSprite** -- repeating texture that fills an area. Supports atlas frames and a `tileRotation` property in v4: -```js -const bg = this.add.tileSprite(400, 300, 800, 600, 'grass'); -// Scroll in update() for parallax backgrounds -bg.tilePositionX += 0.5; -bg.tilePositionY += 0.25; -bg.tileRotation = 0.1; // v4: rotate the tile pattern -``` - -**Blitter** -- high-performance batched rendering for large numbers of static/semi-static sprites. Children (Bobs) have no rotation, scale, or physics, but render significantly faster: -```js -const blitter = this.add.blitter(0, 0, 'particles'); -for (let i = 0; i < 1000; i++) { - const bob = blitter.create( - Phaser.Math.Between(0, 800), - Phaser.Math.Between(0, 600), - 'particle_01' // frame name within the texture - ); - bob.setAlpha(Math.random()); -} -``` - -**Video** -- video playback as a game object with position, scale, and filter support: -```js -this.load.video('intro', 'intro.mp4'); // in preload -const video = this.add.video(400, 300, 'intro'); // in create -video.play(); -``` -</specialized_game_objects> -</sprites_and_images> diff --git a/skills/base44-platform-games/references/phaser/text_and_bitmaptext.md b/skills/base44-platform-games/references/phaser/text_and_bitmaptext.md deleted file mode 100644 index 972c4dc..0000000 --- a/skills/base44-platform-games/references/phaser/text_and_bitmaptext.md +++ /dev/null @@ -1,249 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: text and bitmaptext - -<text_and_bitmaptext> -Use this when displaying text in Phaser 4: the Canvas-based Text game object, TextStyle configuration, word wrap, BitmapText (static and dynamic), retro fonts, alignment, text bounds, and padding. - -In Base44 games this section applies to IN-WORLD text: damage numbers, floating labels, speech bubbles anchored to characters, per-frame counters rendered inside the world. Screen-space UI text — menus, score/lives HUD, game-over screens — is built in the React overlay instead (see the screen_space_ui rules in react_phaser_architecture). - -<quick_start> -```js -// Canvas-based Text (flexible styling, uses browser fonts) -const title = this.add.text(400, 50, 'Hello World', { - fontFamily: 'Arial', - fontSize: '32px', - color: '#ffffff' -}); - -// BitmapText (pre-rendered font texture, faster rendering) -// Font must be loaded first: this.load.bitmapFont('myFont', 'font.png', 'font.xml') -const score = this.add.bitmapText(400, 100, 'myFont', 'Score: 0', 32); - -// DynamicBitmapText (per-character manipulation via callback) -const fancy = this.add.dynamicBitmapText(400, 200, 'myFont', 'Wavy!', 32); -fancy.setDisplayCallback(function (data) { - data.y += Math.sin(data.index * 0.5 + fancy.scene.time.now * 0.005) * 10; - return data; -}); -``` -</quick_start> - -<core_concepts> -**Text vs BitmapText.** `Text` renders via the Canvas 2D API (uploaded as a texture); supports web/CSS fonts, shadows, gradients, strokes, built-in word wrap, justify alignment, and RTL; slower (re-creates the canvas texture on every change). `BitmapText` uses a pre-rendered font texture; no web fonts; drop shadow only (WebGL); `setMaxWidth` wrap; per-character/word tinting (WebGL); fast batched rendering; alignment via `ALIGN_LEFT` (0), `ALIGN_CENTER` (1), `ALIGN_RIGHT` (2). `DynamicBitmapText` is the same as BitmapText plus a per-character `setDisplayCallback` for animation effects (moderate cost). - -Rule of thumb: use `Text` for infrequently-updated in-world text needing flexible styling or web fonts. Use `BitmapText` for in-world text that updates frequently or is rendered in large quantities (damage numbers, combo counters). Use `DynamicBitmapText` only when you need per-character animation. - -**TextStyle.** The `Text` object delegates all styling to its `TextStyle` instance (`text.style`). Pass a style config when creating the Text, or update it later with setter methods; the style object uses Canvas 2D properties internally. Defaults: `fontFamily` `'Courier'`, `fontSize` `'16px'`, `color` `'#fff'`. The `font` shorthand (e.g. `'bold 24px Arial'`) overrides `fontFamily`, `fontSize`, and `fontStyle` when provided. Other notable style properties: `stroke` + `strokeThickness`, `backgroundColor`, `align` (`left`/`right`/`center`/`justify`), `maxLines`, `fixedWidth` / `fixedHeight`, `resolution`, `rtl`, `wordWrap` (`{ width, callback, callbackScope, useAdvancedWrap }`), `shadow` (`{ offsetX, offsetY, color, blur, stroke, fill }`), `padding`, `lineSpacing`, `letterSpacing`, `metrics` (pre-computed `{ ascent, descent, fontSize }` to skip measurement). -</core_concepts> - -<common_patterns> -Basic Text creation: -```js -const label = this.add.text(100, 100, 'Player 1', { - fontFamily: 'Georgia', fontSize: '24px', color: '#00ff00' -}); - -// font shorthand (sets fontStyle, fontSize, fontFamily) -const bold = this.add.text(100, 150, 'Bold Text', { font: 'bold 28px Arial' }); - -// Array of strings creates multi-line text -const multi = this.add.text(100, 200, ['Line 1', 'Line 2', 'Line 3'], { - fontFamily: 'Arial', fontSize: '18px', color: '#ffffff' -}); - -// Text origin defaults to (0, 0) -- top-left. Center it: -label.setOrigin(0.5); -``` - -Styling Text (all setters return `this`): -```js -const text = this.add.text(400, 300, 'Styled', { - fontFamily: 'Verdana', fontSize: '48px', color: '#ff0000', - stroke: '#000000', strokeThickness: 4, backgroundColor: '#333333', - shadow: { offsetX: 2, offsetY: 2, color: '#000', blur: 4, stroke: false, fill: true } -}); - -text.setColor('#00ffff'); -text.setFontSize(64); -text.setFontFamily('Courier'); -text.setFontStyle('italic'); -text.setStroke('#ff00ff', 6); -text.setShadow(3, 3, '#000', 5, false, true); -text.setBackgroundColor('#222'); -text.setStyle({ fontSize: '64px', fontFamily: 'Arial', color: '#ffffff', align: 'center' }); // replace all - -// Canvas gradients and patterns work as fill/stroke -const gradient = text.context.createLinearGradient(0, 0, 0, text.height); -gradient.addColorStop(0, '#ff0000'); -gradient.addColorStop(1, '#0000ff'); -text.setFill(gradient); -``` - -Word wrap and alignment: -```js -const wrapped = this.add.text(50, 50, 'Long text here...', { - fontFamily: 'Arial', fontSize: '20px', color: '#fff', wordWrap: { width: 300 } -}); -wrapped.setWordWrapWidth(300); -wrapped.setWordWrapWidth(300, true); // advanced: collapses spaces, trims whitespace -wrapped.setWordWrapCallback(function (text, textObject) { - return text.split(' ').join('\n'); // return string with \n or array of lines -}); -const lines = wrapped.getWrappedText(); - -// Alignment only affects multi-line text: 'left' (default), 'right', 'center', 'justify' -const aligned = this.add.text(400, 100, 'Line 1\nLonger Line 2\nLine 3', { - fontFamily: 'Arial', fontSize: '24px', color: '#fff', align: 'center' -}); -aligned.setAlign('right'); -// For center alignment to look correct, combine with a fixed width: -aligned.setFixedSize(300, 0); -aligned.setAlign('center'); -``` - -Dynamic updates, padding, fixed size, spacing: -```js -const comboText = this.add.text(10, 10, 'Combo: 0', { fontFamily: 'Arial', fontSize: '24px', color: '#fff' }); -comboText.setText('Combo: 12'); -comboText.setText(['Combo: 12', 'Best: 30']); // joins with \n -comboText.appendText('Extra line'); // prepends \n by default -comboText.appendText(' more', false); // no carriage return -const current = comboText.text; - -// Padding adds space around text inside the canvas -const padded = this.add.text(100, 100, 'Padded', { - fontFamily: 'Arial', fontSize: '24px', color: '#fff', backgroundColor: '#333', - padding: { left: 10, right: 10, top: 5, bottom: 5 } -}); -padded.setPadding({ x: 10, y: 5 }); // shorthand: x=left+right, y=top+bottom -padded.setPadding(10, 5, 10, 5); // left, top, right, bottom -padded.setPadding(10); // single value = all sides - -// Fixed dimensions create a text canvas of exact size -const fixed = this.add.text(100, 100, 'Fixed box', { - fontFamily: 'Arial', fontSize: '18px', color: '#fff', - fixedWidth: 200, fixedHeight: 100, wordWrap: { width: 200 }, align: 'center' -}); -fixed.setMaxLines(3); - -// Line and letter spacing -const spaced = this.add.text(100, 200, 'Spaced\nLines', { - fontFamily: 'Arial', fontSize: '24px', color: '#fff', lineSpacing: 10, letterSpacing: 2 -}); -spaced.setLineSpacing(20); -spaced.setLetterSpacing(5); -``` - -BitmapText creation, alignment, shadow, tinting: -```js -// Load in preload: this.load.bitmapFont('pixelFont', 'font.png', 'font.xml'); -const bmpText = this.add.bitmapText(100, 100, 'pixelFont', 'Hello', 32); - -// align: 0 = left, 1 = center, 2 = right -const aligned = this.add.bitmapText(100, 200, 'pixelFont', 'Line 1\nLine 2', 24, 1); -bmpText.setLeftAlign(); -bmpText.setCenterAlign(); -bmpText.setRightAlign(); - -bmpText.setText('Updated!'); // or: bmpText.text = 'Also works'; -bmpText.setFontSize(48); // or: bmpText.fontSize = 48; -bmpText.setLetterSpacing(2); -bmpText.setLineSpacing(5); -bmpText.setMaxWidth(200); // word wrap by max pixel width - -// Drop shadow (WebGL only, static BitmapText only); call with no args to clear -bmpText.setDropShadow(2, 2, 0x000000, 0.5); -bmpText.setDropShadow(); - -// Tint characters/words (WebGL only) -// setCharacterTint(start, length, tintMode, color) -- length -1 = all from start -bmpText.setCharacterTint(0, 5, Phaser.TintModes.MULTIPLY, 0xff0000); -bmpText.setWordTint('Score', -1, Phaser.TintModes.MULTIPLY, 0x00ff00); // by string match or word index -``` - -DynamicBitmapText: -```js -const dynamic = this.add.dynamicBitmapText(100, 100, 'pixelFont', 'Dynamic!', 32); - -// Per-character display callback -- invoked each render frame per character -// data: { parent, tint, index, charCode, x, y, scale, rotation, data } -dynamic.setDisplayCallback(function (data) { - data.x += Math.sin(data.index + dynamic.scene.time.now * 0.01) * 5; - data.y += Math.cos(data.index + dynamic.scene.time.now * 0.01) * 5; - return data; -}); - -// Scrolling text window -dynamic.setSize(200, 50); // crop region in pixels -dynamic.setScrollX(10); -dynamic.setScrollY(0); -``` - -Retro fonts use a fixed-width character grid from a standard image (no XML/JSON font file): -```js -this.load.image('retroFont', 'retroFont.png'); // in preload - -const config = { - image: 'retroFont', - width: 8, height: 8, // character size in pixels - chars: Phaser.GameObjects.RetroFont.TEXT_SET1, - charsPerRow: 16, - offset: { x: 0, y: 0 }, spacing: { x: 0, y: 0 }, lineSpacing: 0 -}; -this.cache.bitmapFont.add('retroFont', { data: Phaser.GameObjects.RetroFont.Parse(this, config) }); -const retro = this.add.bitmapText(100, 100, 'retroFont', 'RETRO TEXT', 8); // use like any BitmapText -``` -Available `Phaser.GameObjects.RetroFont` TEXT_SET constants: `TEXT_SET1` (full ASCII printable), `TEXT_SET2` (space through Z), `TEXT_SET3`-`TEXT_SET11` (subsets of uppercase, digits, punctuation). `TEXT_SET10` is uppercase letters only. - -Text bounds (BitmapText) and high-DPI resolution: -```js -const bounds = bmpText.getTextBounds(); -// bounds.local -- { x, y, width, height } at origin 0,0 -// bounds.global -- with scale + world position -// bounds.lines -- per-line rects -// bounds.words -- word objects with positions -// bounds.characters -- character objects with positions -console.log(bmpText.width, bmpText.height); // read-only, computed from global bounds -const char = bmpText.getCharacterAt(pointer.worldX, pointer.worldY); // for click detection - -// Higher resolution for crisp text on HiDPI displays (costs more memory) -const crisp = this.add.text(100, 100, 'Sharp!', { - fontFamily: 'Arial', fontSize: '24px', color: '#fff', resolution: 2 -}); -crisp.setResolution(window.devicePixelRatio); -``` -</common_patterns> - -<factory_methods> -```js -// Text -- Canvas-based. text: string | string[]; style: optional. Returns Phaser.GameObjects.Text -this.add.text(x, y, text, style); - -// BitmapText -- static, fast. font: cache key; text: optional; size: optional (defaults to font data size); align: optional (0). Returns Phaser.GameObjects.BitmapText -this.add.bitmapText(x, y, font, text, size, align); - -// DynamicBitmapText -- per-character callback. Same args as bitmapText. Returns Phaser.GameObjects.DynamicBitmapText -this.add.dynamicBitmapText(x, y, font, text, size); -``` -BitmapText alignment constants: `BitmapText.ALIGN_LEFT` (0), `BitmapText.ALIGN_CENTER` (1), `BitmapText.ALIGN_RIGHT` (2). -</factory_methods> - -<gotchas> -- **Text re-renders on every change.** Each `setText`/`setStyle`/`setColor` re-creates the internal canvas and (in WebGL) re-uploads the texture. Batch style changes -- set the style object properties directly and call `updateText()` once at the end. -- **Font must be loaded before use.** Text uses the Canvas `fillText` API, so fonts must be available in the browser (CSS, a web font loader, or already installed). Phaser does not load web fonts. -- **Font names with special characters need quotes.** Names with digits or special characters must be double-quoted: `fontFamily: '"Press Start 2P"'`. -- **Text origin defaults to (0, 0)** (top-left), unlike Sprites (0.5, 0.5). Call `setOrigin(0.5)` to center. -- **Alignment only affects multi-line text.** `align: 'center'` does nothing on a single line -- use `setOrigin(0.5)` or manual positioning. -- **BitmapText font must be in cache.** The key passed to `this.add.bitmapText()` must match a key loaded via `this.load.bitmapFont()`, else a console warning and undefined behavior. -- **BitmapText `setMaxWidth` only wraps on whitespace.** If no whitespace (default char code 32) is found, no wrapping occurs. Change the wrap character with the second argument. -- **DynamicBitmapText is more expensive** -- the display callback runs for every character every frame. Use only when you need per-character manipulation. -- **`setCharacterTint` and `setWordTint` are WebGL only** -- no effect in Canvas renderer. -- **`setDropShadow` is WebGL only and static BitmapText only** -- not DynamicBitmapText or Canvas renderer. -- **`letterSpacing` on Text is expensive** -- non-zero spacing renders each character individually with `fillText`. Use BitmapText for large amounts of letter-spaced text. -- **`setFill` and `setColor` are equivalent** -- both set the `color` property; the style config also accepts `fill` as an alias. -- **`setRTL` must be called BEFORE `setText`.** RTL mode changes how the internal canvas is constructed. Set it in the style config or call `setRTL()` before any `setText()`. -- **BitmapText is much faster for frequently updating content.** Text re-renders the whole canvas and re-uploads the GPU texture on every change; BitmapText just updates vertex data. For damage numbers, combo counters, or any per-frame in-world text, always prefer BitmapText. (Score/timer HUD readouts belong in the React overlay, not Phaser text.) -</gotchas> -</text_and_bitmaptext> diff --git a/skills/base44-platform-games/references/phaser/tilemaps.md b/skills/base44-platform-games/references/phaser/tilemaps.md deleted file mode 100644 index 06fc6d1..0000000 --- a/skills/base44-platform-games/references/phaser/tilemaps.md +++ /dev/null @@ -1,218 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: tilemaps - -<tilemaps> -Use this when working with tilemaps in Phaser 4: loading Tiled JSON maps, creating tilemap layers, tile collision, dynamic tiles, tile properties, object layers, and orthogonal/isometric/hexagonal/staggered maps. - -<quick_start> -A `Tilemap` holds parsed map data (layers, tilesets, objects) and provides methods to add tilesets, create layers, set collision, and query tiles. Layers (`TilemapLayer` or `TilemapGPULayer`) are the Game Objects that actually render tiles. - -```js -class GameScene extends Phaser.Scene { - preload() { - // Load the Tiled JSON and the tileset image - this.load.tilemapTiledJSON('map', 'assets/level1.json'); - this.load.image('tiles', 'assets/tilesheet.png'); - } - - create() { - // Create the tilemap from cached JSON - const map = this.add.tilemap('map'); - - // Link the tileset image to the tileset name used in Tiled - const tileset = map.addTilesetImage('tilesheet', 'tiles'); - - // Create a layer - layerID must match the layer name in Tiled - const ground = map.createLayer('Ground', tileset); - - // Enable collision on specific tile indexes - ground.setCollision([1, 2, 3]); - } -} -``` - -The flow is always: load JSON + image, create tilemap, add tileset image, create layer(s), set collision. -</quick_start> - -<core_concepts> -**Tilemap vs Layer.** A `Tilemap` is a data container, not a display object -- it stores parsed map data and provides methods that operate on it. A `TilemapLayer` or `TilemapGPULayer` is the actual Game Object added to the display list that renders tiles. `this.add.tilemap(key)` is a factory on `GameObjectFactory` that delegates to `ParseToTilemap`, reading from the cache and returning a `Tilemap`. - -```js -const map = this.add.tilemap('map'); // Data container (not rendered) -const layer = map.createLayer('Ground', tileset); // Game Object (rendered) -``` - -**Tilesets.** A `Tileset` links a tileset name (from Tiled) to a loaded texture; it stores `firstgid`, tile dimensions, margin, and spacing. -```js -// tilesetName: the name in Tiled's tileset panel; key: the Phaser texture key (defaults to tilesetName) -const tileset = map.addTilesetImage('tilesetName', 'textureKey'); -// Override tile dimensions, margin, and spacing if needed -const tileset = map.addTilesetImage('name', 'key', 16, 16, 1, 2); -``` -`addTilesetImage(tilesetName, key, tileWidth, tileHeight, tileMargin, tileSpacing, gid, tileOffset)` -- if the tileset name already exists in the parsed map data, it updates the existing Tileset with the texture; otherwise (non-Tiled maps) it creates a new one. The Phaser Tiled parser does NOT support "Collection of Images" tilesets -- all tiles must be in a single tileset image per tileset. - -**The Tile class.** Each cell in a layer is a `Tile`. Key properties: `index` (tile index in the tileset, -1 for empty), `x`/`y` (tile coordinates, not pixels), `pixelX`/`pixelY` (pixel position relative to layer origin), `width`/`height` (pixels), `properties` (custom properties object from Tiled), `collideLeft`/`Right`/`Up`/`Down` (per-edge collision flags), `faceLeft`/`Right`/`Top`/`Bottom` (interesting-face flags for collision optimization), `collisionCallback` (per-tile callback), `tint` (default `0xffffff`), `tintMode` (default `TintModes.MULTIPLY`), `rotation`, `physics` (engine-specific data e.g. bodies), and inherited `alpha`/`visible`/`flipX`/`flipY`. - -**TilemapGPULayer (v4.0.0)** is a high-performance WebGL-only alternative to `TilemapLayer`, rendering the entire layer as a single quad via a shader (almost entirely GPU-bound). Pass `gpu: true` as the 5th arg to `createLayer`: -```js -const layer = map.createLayer('Ground', tileset, 0, 0, true); -``` -Capabilities: single tileset per layer only, max 4096x4096 tiles, max unique tile IDs 2^23, supports tile flip and tile animation, orthographic maps only, smooth borders with LINEAR filtering or sharp pixels with NEAREST. Restrictions: layer edits do NOT display automatically -- call `generateLayerDataTexture()` after modifying tiles; WebGL only (no Canvas fallback); cannot use multiple tilesets. -```js -gpuLayer.putTileAt(5, 10, 10); -gpuLayer.generateLayerDataTexture(); // required after edits -``` - -Both `TilemapLayer` and `TilemapGPULayer` extend `TilemapLayerBase` (which extends `GameObject`). The base class provides all tile query, manipulation, and collision methods, with these mixins: Alpha, BlendMode, ComputedSize, Depth, ElapseTimer, Flip, GetBounds, Lighting, Mask, Origin, RenderNodes, Transform, Visible, ScrollFactor, and Arcade Physics Collision. -</core_concepts> - -<common_patterns> -Creating from Tiled JSON and multiple layers: -```js -const map = this.add.tilemap('map'); -const tileset = map.addTilesetImage('TilesetNameInTiled', 'tiles'); -const layer = map.createLayer('LayerNameInTiled', tileset); - -// Multiple layers render in creation order; use depth for finer control -const background = map.createLayer('Background', tileset); -const ground = map.createLayer('Ground', tileset); -const foreground = map.createLayer('Foreground', tileset); -foreground.setDepth(10); - -// A CPU layer can use multiple tilesets -const tiles1 = map.addTilesetImage('terrain', 'terrain-img'); -const tiles2 = map.addTilesetImage('objects', 'objects-img'); -const multi = map.createLayer('Ground', [tiles1, tiles2]); -``` -The `layerID` passed to `createLayer` must match the layer name in Tiled exactly. Group layer children are flattened with a `'ParentGroup/Layer'` naming convention. - -Blank layer for procedural maps: -```js -// createBlankLayer(name, tileset, x, y, width, height, tileWidth, tileHeight) -const layer = map.createBlankLayer('dynamic', tileset, 0, 0, 50, 50, 32, 32); -layer.fill(1); // fill entire layer with tile index 1 -layer.putTileAt(5, 10, 10); // place tile index 5 at tile coord (10, 10) -``` - -Collision setup (several ways): -```js -layer.setCollision([1, 2, 3]); // by specific indexes -layer.setCollisionBetween(1, 50); // by range (inclusive) -layer.setCollisionByProperty({ collides: true }); // by Tiled property; supports arrays: { type: ['stone', 'lava'] } -layer.setCollisionByExclusion([-1, 0]); // collide on all tiles EXCEPT these (-1 empty, 0 often background) -layer.setCollisionFromCollisionGroup(); // from Tiled collision editor shapes -``` -All collision methods on `TilemapLayerBase` mirror methods on `Tilemap` but without a `layer` param. On the `Tilemap` you can use the "current layer" or specify one explicitly: -```js -map.setLayer('Ground'); -map.setCollision([1, 2, 3]); // applies to current layer -map.setCollision([1, 2, 3], true, true, 'Ground'); // explicit layer -``` - -Physics integration (Arcade) -- the layer must have collision set for physics to detect it: -```js -this.physics.add.collider(player, groundLayer); -this.physics.add.collider(player, groundLayer, (sprite, tile) => { - if (tile.index === 5) { /* hit a special tile */ } -}); -this.physics.add.overlap(player, groundLayer, (sprite, tile) => { /* overlapping */ }); -``` -The layer has `collisionCategory` and `collisionMask` properties for collision filtering. - -Tile properties and callbacks (callbacks require an active physics collider/overlap between body and layer): -```js -const tile = layer.getTileAt(10, 5); -console.log(tile.properties.damage, tile.properties.type); // custom props from Tiled -layer.setCollisionByProperty({ type: ['wall', 'rock'] }); - -// Callback by tile index -- fires when a physics body overlaps these tiles -map.setTileIndexCallback([5, 6, 7], (sprite, tile) => { /* ... */ }, this); -// Callback by tile location -- fires for tiles in a rectangular area -map.setTileLocationCallback(10, 10, 5, 5, (sprite, tile) => { /* ... */ }, this); -// Per-tile callback -layer.getTileAt(10, 5).collisionCallback = (sprite, tile) => { /* ... */ }; -``` - -Querying tiles: -```js -const tile = layer.getTileAt(10, 5); // by tile coords (or null) -const tile = layer.getTileAt(10, 5, true); // nonNull: Tile with index -1 instead of null -const tile = layer.getTileAtWorldXY(worldX, worldY); // by world coords -const exists = layer.hasTileAt(10, 5); - -const tiles = layer.getTilesWithin(0, 0, 10, 10); // tile coord region -const tiles = layer.getTilesWithinWorldXY(x, y, w, h); // world coord region -const tiles = layer.getTilesWithinShape(circle); // shape overlap - -const water = layer.filterTiles(t => t.properties.type === 'water'); -const spawn = layer.findTile(t => t.properties.isSpawn); -layer.forEachTile(t => { /* iterate all tiles */ }); -``` - -Modifying tiles at runtime: -```js -layer.putTileAt(5, 10, 10); // place tile index 5 at (10, 10) -layer.putTileAtWorldXY(5, worldX, worldY); // place by world coords -layer.putTilesAt([[1, 2], [3, 4]], 10, 10); // place a 2x2 grid -layer.removeTileAt(10, 10); -layer.fill(1, 0, 0, 10, 10); // fill 10x10 region with index 1 -layer.replaceByIndex(5, 10); // replace all index-5 with index-10 -layer.copy(0, 0, 5, 5, 20, 20); // copy 5x5 from (0,0) to (20,20) -layer.randomize(0, 0, 10, 10, [1, 2, 3, 4]); -layer.weightedRandomize([{ index: 1, weight: 4 }, { index: 2, weight: 1 }], 0, 0, 10, 10); -layer.shuffle(0, 0, 10, 10); -``` - -Coordinate conversion: -```js -const tileXY = layer.worldToTileXY(worldX, worldY); -const worldXY = layer.tileToWorldXY(tileX, tileY); - -// Reuse a vector to avoid allocation -const vec = new Phaser.Math.Vector2(); -layer.worldToTileXY(worldX, worldY, true, vec); // snapToFloor = true -``` - -Object layers (Tiled) -- use `createFromObjects` to turn points/rectangles/sprite placements into Game Objects: -```js -// Match by tile GID, with a custom class extending Sprite -const enemies = map.createFromObjects('Enemies', { gid: 26, classType: Enemy }); -// Match by name -const coins = map.createFromObjects('Items', { name: 'coin', key: 'coin-texture', frame: 0 }); -// Match by type -const spawns = map.createFromObjects('Spawns', { type: 'player-spawn' }); - -// Access raw object layer data -const objectLayer = map.getObjectLayer('Enemies'); -objectLayer.objects.forEach(obj => console.log(obj.name, obj.x, obj.y, obj.properties)); -``` -`createFromObjects(layerName, config, useTileset)` config options: `id`, `gid`, `name`, `type`, `classType` (default `Sprite`), `scene`, `container`, `key`, `frame`, `ignoreTileset`. - -Animated tiles are defined in Tiled's tileset editor and parsed automatically. Both layer types support them; `TilemapLayerBase` uses `ElapseTimer` to track animation time via `preUpdate`. - -Isometric, hexagonal, and staggered maps -- the map `orientation` property is set from Tiled data and coordinate conversion functions are selected automatically: -```js -const map = this.add.tilemap('iso-map'); -const tileset = map.addTilesetImage('iso-tiles', 'iso-img'); -const layer = map.createLayer('Ground', tileset); -const tile = layer.getIsoTileAtWorldXY(worldX, worldY); // tile at world coords in iso space -// TilemapGPULayer does NOT support iso/hex/staggered -- use TilemapLayer -``` -</common_patterns> - -<gotchas> -- **Tileset name must match Tiled exactly.** The first arg to `addTilesetImage` is the tileset name as defined in Tiled, not the Phaser texture key. Mismatch returns `null` + a console warning. -- **Layer name must match Tiled exactly.** `createLayer` takes the layer name (or index). Group layer children are prefixed `'GroupName/LayerName'`. -- **Each layer can only be created once.** Calling `createLayer` with the same layer ID twice returns `null` + a warning -- the layer data can only be associated with one Game Object. -- **`setCollision` must be called before physics colliders work.** Without marking tiles as collidable, `this.physics.add.collider()` passes through all tiles. -- **TilemapGPULayer is orthographic only** -- no isometric/hexagonal/staggered, and only a single tileset per layer. -- **TilemapGPULayer requires manual texture regeneration.** After `putTileAt` or other edits, call `generateLayerDataTexture()` or the changes won't appear. -- **"Collection of Images" tilesets are not supported.** The Tiled parser requires all tiles in a tileset to be in a single image; embedded tilesets in the exported JSON are required. -- **Tile index -1 means empty.** Many methods return `null` for empty tiles by default. Pass `nonNull: true` to get a Tile object with `index === -1` instead. -- **`insertNull` in the tilemap factory.** `insertNull: true` stores `null` for empty tiles instead of Tile objects with index -1. Saves memory for large sparse maps but prevents dynamic tile placement in empty cells. -- **Tile callbacks only fire with active physics.** `setTileIndexCallback` and `setTileLocationCallback` require a physics collider or overlap between the body and the layer. -- **Layer position and Tiled offset.** If `x` and `y` are not specified in `createLayer`, they default to the layer offset defined in Tiled, not (0, 0). -</gotchas> -</tilemaps> diff --git a/skills/base44-platform-games/references/phaser/time_and_timers.md b/skills/base44-platform-games/references/phaser/time_and_timers.md deleted file mode 100644 index bb7f8eb..0000000 --- a/skills/base44-platform-games/references/phaser/time_and_timers.md +++ /dev/null @@ -1,239 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: time and timers - -<time_and_timers> -Use this when working with timers and time-based events in Phaser 4: the Clock plugin, TimerEvents, delayed calls, looping timers, Timeline event sequencing, pausing time, time scaling, and delta time. - -<quick_start> -`this.time` is the scene's `Clock` instance (registered as the `'Clock'` plugin under the key `time`). It creates and manages `TimerEvent` objects that fire callbacks based on game time. - -```js -// In a Scene's create() method: - -// One-shot delayed call (fires once after 1 second) -this.time.delayedCall(1000, () => { - console.log('One second later'); -}); - -// Repeating timer (fires 5 times, once every 500ms) -this.time.addEvent({ - delay: 500, - callback: () => { console.log('tick'); }, - repeat: 4 // 4 repeats = 5 total fires -}); - -// Infinite loop timer -this.time.addEvent({ - delay: 1000, - callback: this.spawnEnemy, - callbackScope: this, - loop: true -}); -``` -</quick_start> - -<core_concepts> -The Clock (`this.time`) is a Scene-level plugin that tracks game time and updates all of its TimerEvents each frame. Key properties: `now` (current time in ms, equivalent to `time` passed to the scene `update` method), `startTime` (timestamp when the scene started), `timeScale` (multiplier applied to delta time, default `1` — above 1 speeds up all timers, below 1 slows them down, `0` freezes time), and `paused` (when `true`, no TimerEvents are updated). The Clock listens to `PRE_UPDATE` (to flush pending additions/removals) and `UPDATE` (to tick active events). It is automatically shut down and destroyed with the scene. - -A TimerEvent accumulates elapsed time each frame: `elapsed += delta * clock.timeScale * event.timeScale`. When `elapsed >= delay`, the callback fires. After all repeats are exhausted the event is removed from the Clock on the next frame. Config keys: `delay`, `repeat`, `loop`, `callback`, `callbackScope`, `args`, `timeScale`, `startAt`, `paused`. TimerEvent and Clock do not emit EventEmitter events — timers use the `callback` property directly. - -A Timeline (`this.add.timeline`) is a sequencer for scheduling actions at specific points in time. Unlike the Clock (which manages independent timers), a Timeline runs a linear sequence of events keyed by absolute or relative timestamps: - -```js -const timeline = this.add.timeline([ - { at: 0, run: () => { /* immediate */ } }, - { at: 1000, run: () => { /* at 1s */ } }, - { at: 2500, tween: { targets: sprite, alpha: 0, duration: 500 } } -]); -timeline.play(); -``` - -Timelines always start **paused** — you must call `play()` to start them. They are created via the GameObjectFactory and destroyed automatically when the scene shuts down. -</core_concepts> - -<common_patterns> -Delayed call shorthand (fires once, no repeat): -```js -this.time.delayedCall(2000, () => { - this.scene.start('GameOver'); -}); -``` - -Repeating timer with a finite count (`repeat: 9` means 10 total fires — 1 initial + 9 repeats): -```js -const timer = this.time.addEvent({ - delay: 200, - callback: this.fireBullet, - callbackScope: this, - repeat: 9 -}); - -timer.getRepeatCount(); // repeats remaining -timer.getOverallProgress(); // 0..1 across all repeats -``` - -Infinite loop (`repeat: -1` is equivalent to `loop: true`): -```js -const spawner = this.time.addEvent({ - delay: 3000, - callback: this.spawnWave, - callbackScope: this, - loop: true -}); - -spawner.remove(); // stop it later, or spawner.paused = true to pause -``` - -First-fire shortcut with `startAt` (pre-fills elapsed so the first fire happens sooner): -```js -// First fire after 100ms, then every 2s -this.time.addEvent({ - delay: 2000, - callback: this.heartbeat, - callbackScope: this, - loop: true, - startAt: 1900 -}); -``` - -Stopping and removing timers: -```js -const timer = this.time.addEvent({ delay: 1000, loop: true, callback: fn }); - -timer.remove(); // schedules removal next frame, silently expires -timer.remove(true); // fires callback one last time, then expires -this.time.removeEvent(timer); -this.time.removeAllEvents(); -``` - -Pausing and resuming — pause the entire Clock (`this.time.paused = true`) to freeze all timers, or pause a single timer (`timer.paused = true`). - -Time scaling (slow motion / fast forward) — effective scale is `clock.timeScale * event.timeScale`: -```js -this.time.timeScale = 0.5; // slow all timers in this scene to half speed -timer.timeScale = 2; // speed up a single timer to 2x (net 1x here) -``` - -Reading timer state: -```js -timer.getProgress(); // 0..1 for current iteration -timer.getOverallProgress(); // 0..1 across all repeats -timer.getElapsed(); // ms elapsed this iteration -timer.getElapsedSeconds(); // seconds elapsed this iteration -timer.getRemaining(); // ms until next fire -timer.getRemainingSeconds(); // seconds until next fire -timer.getOverallRemaining(); // ms until final fire -timer.getOverallRemainingSeconds(); // seconds until final fire -timer.getRepeatCount(); // repeats left -``` - -A completed `TimerEvent` can be reset with a new config and re-added to the Clock — `reset()` alone does not schedule it: -```js -const timer = this.time.addEvent({ - delay: 1000, callback: this.phase1, callbackScope: this, repeat: 2 -}); - -// Later, after it completes, reconfigure and re-add: -timer.reset({ - delay: 500, callback: this.phase2, callbackScope: this, repeat: 4 -}); -this.time.addEvent(timer); // must re-add; reset() does not schedule it -``` -</common_patterns> - -<timelines> -Timelines excel at sequencing mixed actions — callbacks, tweens, sounds, property sets, and custom events: -```js -const timeline = this.add.timeline([ - { - at: 0, - run: () => { this.title.setAlpha(1); }, - sound: 'intro' - }, - { - at: 2000, - tween: { targets: this.title, y: 100, duration: 1000 }, - sound: { key: 'whoosh', config: { volume: 0.5 } } - }, - { - at: 4000, - set: { alpha: 0 }, - target: this.title, - event: 'INTRO_DONE' - } -]); - -timeline.on('INTRO_DONE', (target) => { /* custom event */ }); -timeline.on('complete', (tl) => { /* all events ran */ }); -timeline.play(); -``` - -Relative timing — `at` is absolute ms from timeline start (default 0); `in` is an offset from the current `elapsed` time (useful when adding events to a running timeline); `from` is an offset from the previous event's start time. Priority: `from` overrides `in`, which overrides `at`. Each `from` is relative to the previous event's resolved time, not the previous `from` value, and events are processed in array order. -```js -const timeline = this.add.timeline([ - { at: 1000, run: stepOne }, // absolute: 1s from start - { from: 500, run: stepTwo }, // relative: 500ms after previous (1.5s) - { from: 1000, run: stepThree } // relative: 1000ms after previous (2.5s) -]); -``` - -Conditional events — if the `if` callback returns `false`, the event is skipped (marked complete but its actions are not executed): -```js -{ at: 5000, if: () => this.player.health > 0, run: () => { this.showBonusRound(); } } -``` - -Looping — the `loop` callback on a TimelineEventConfig fires on repeat iterations (not the first run): -```js -timeline.repeat().play(); // infinite loop -timeline.repeat(3).play(); // loop 3 additional times (4 total runs) -timeline.repeat(false).play(); // no looping -``` - -Control: -```js -timeline.pause(); // freezes elapsed counter and pauses spawned tweens -timeline.resume(); // resumes elapsed counter and unpauses spawned tweens -timeline.stop(); // sets paused=true, complete=true -timeline.reset(); // elapsed=0, all events marked incomplete, starts playing -timeline.isPlaying(); // true if not paused and not complete -timeline.getProgress();// 0..1 based on events completed / total events -timeline.timeScale = 2; // double speed (does NOT affect spawned tweens) -``` - -TimelineEventConfig keys: `at` / `in` / `from` (timing), `run` (callback), `loop` (repeat-iteration callback), `if` (guard, return `false` to skip), `event` (emitted on the Timeline with signature `(target)`), `target` (scope for `run`/`loop`/`if` and target for `set`), `set` (key-value pairs applied to `target`), `tween` (config or instance created when the event fires), `sound` (key or `{ key, config }`), `once` (remove this event after it fires), `stop` (stop the entire timeline when this event fires). The Timeline `'complete'` event fires when all events have run. - -Full cutscene example: -```js -class CutsceneScene extends Phaser.Scene { - create() { - const timeline = this.add.timeline([ - { at: 0, run: () => { console.log('Start!'); } }, - { at: 1000, tween: { targets: this.player, x: 400, duration: 500, ease: 'Power2' } }, - { at: 1500, sound: 'doorOpen' }, - { at: 2000, set: { visible: true }, target: this.door }, - { from: 500, run: () => { console.log('Relative timing'); } }, - { at: 5000, event: 'cutsceneDone', stop: true } - ]); - - timeline.on('cutsceneDone', () => { this.scene.start('GameScene'); }); - timeline.play(); - } -} -``` -</timelines> - -<gotchas> -- **repeat vs total fires.** `repeat: 4` means 5 total callback invocations (1 initial + 4 repeats). A common off-by-one source. -- **Zero delay with repeat throws.** A `TimerEvent` with `delay: 0` and any repeat/loop throws `'TimerEvent infinite loop created via zero delay'`. -- **Timelines start paused.** You must call `timeline.play()` after creation. Forgetting this is a frequent mistake. -- **Timeline timeScale does not affect tweens.** It only scales the timeline's own elapsed counter; tweens created by the timeline run at their own speed. Set tween `timeScale` separately or use the TweenManager. -- **`once` events are removed permanently.** Timeline events with `once: true` are spliced out after firing and will not reappear on `reset()` or when looping. -- **Timer additions are deferred.** `addEvent()` pushes to a pending list processed in `preUpdate`; the timer is not active until the next frame. You can pass an existing `TimerEvent` to `addEvent()`, but it must not be in a completed state. -- **Clock paused vs timeScale 0.** Both freeze timers, but `paused = true` skips the update loop entirely while `timeScale = 0` still runs it with zero delta. Prefer `paused` for a full freeze. -- **callbackScope default.** If you omit `callbackScope`, the TimerEvent itself becomes `this` inside the callback, not the scene. Use arrow functions or pass `callbackScope: this` explicitly. -- **`timer.reset()` does not re-add to Clock.** Calling `timer.reset(config)` reinitializes the timer but does not schedule it — you must call `this.time.addEvent(timer)` again. -- **Scene pause pauses the Timeline.** When a scene is paused (`scene.pause()`), the Timeline's update loop stops too, since Timeline updates are driven by the scene's update step. -</gotchas> -</time_and_timers> diff --git a/skills/base44-platform-games/references/phaser/tweens.md b/skills/base44-platform-games/references/phaser/tweens.md deleted file mode 100644 index 34d14f7..0000000 --- a/skills/base44-platform-games/references/phaser/tweens.md +++ /dev/null @@ -1,173 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Phaser 4: tweens - -<tweens> -Use this when animating properties over time in Phaser 4: tweens, tween chains, easing functions, stagger, yoyo, repeat, callbacks, number tweens, and the TweenManager. - -<quick_start> -`this.tweens` is the scene's `TweenManager`, available in every Scene. `add()` creates a tween, adds it to the manager, and starts playback immediately. - -```js -// In a Scene's create() method: -const logo = this.add.image(100, 300, 'logo'); - -// Move the logo to x:600 over 2 seconds -this.tweens.add({ - targets: logo, - x: 600, - duration: 2000, - ease: 'Power2' -}); -``` -</quick_start> - -<core_concepts> -Tween lifecycle: Created -> Active (`onActive`) -> Start Delayed (`delay`) -> Playing (`onStart`, `onUpdate` per frame) -> Yoyo/Repeat (`onYoyo`, `onRepeat`) -> Loop (`onLoop`) -> Complete (`onComplete`, then auto-destroyed unless `persist: true`). - -Tweens are fire-and-forget. They auto-destroy after completion, so you do not need to store a reference unless you want to control them later. Set `persist: true` to keep a tween alive for replay via `tween.play()` / `tween.restart()` — you must then call `tween.destroy()` yourself when done. - -`targets` accepts a single object, an array of objects, or a function returning either. Targets are usually Game Objects but can be any JS object with numeric properties. A tween will not manipulate any property whose name begins with an underscore. - -Property values support several forms: -```js -this.tweens.add({ - targets: sprite, - x: 400, // absolute value - y: '-=100', // relative (subtract 100 from current) - rotation: '+=3.14', // relative (add to current) - alpha: { value: 0, duration: 300, ease: 'Cubic.easeIn' }, // per-property config - scale: [0.5, 1.5, 1], // array: interpolates through values over duration - angle: function (target, key, value, targetIndex, totalTargets, tween) { - return targetIndex * 90; // function: called once per target - }, - duration: 1000 -}); -``` -Array values use linear interpolation by default; override with the `interpolation` config (`'linear'`, `'bezier'`, `'catmull'`). -</core_concepts> - -<common_patterns> -Multiple properties with per-property config: -```js -this.tweens.add({ - targets: this.enemy, - x: { value: 600, duration: 1500, ease: 'Bounce.easeOut' }, - y: { value: 200, duration: 1000, ease: 'Power2' }, - alpha: { value: 0.5, duration: 500, delay: 1000 } -}); -``` - -Yoyo and repeat: -```js -this.tweens.add({ - targets: this.coin, - y: '-=50', - duration: 600, - ease: 'Sine.easeInOut', - yoyo: true, // returns to start value after reaching end - hold: 200, // pause 200ms at the end value before yoyo-ing back - repeat: -1, // -1 = infinite, 0 = play once, 1 = play twice, etc. - repeatDelay: 300 // pause 300ms before each repeat -}); -``` -`repeat` is per-property (a `repeat` of 1 plays the property twice total). `loop` restarts the entire tween from scratch. Use `repeat` for property-level looping, `loop` for tween-level looping. - -Stagger offsets a value across multiple targets via `this.tweens.stagger()`: -```js -delay: this.tweens.stagger(100) // 100ms between each target -delay: this.tweens.stagger(200, { from: 'center' }) // from center outward -delay: this.tweens.stagger([0, 1000]) // distribute 0-1000ms across targets -delay: this.tweens.stagger(500, { grid: [10, 6], from: 'center', ease: 'Cubic.easeOut' }) -``` -StaggerConfig: `start` (offset), `ease`, `from` (`'first'`/`'center'`/`'last'`/index), `grid` ([w, h]). - -A TweenChain plays tweens in sequence — each starts after the previous completes: -```js -this.tweens.chain({ - targets: this.player, - tweens: [ - { x: 300, duration: 1000, ease: 'Power2' }, - { y: 500, duration: 800, ease: 'Bounce.easeOut' }, - { scale: 2, duration: 500 }, - { alpha: 0, duration: 400 } - ], - loop: 1, // loop the entire chain once (plays twice total) - loopDelay: 500, - onComplete: function () { } -}); -``` -Chain-level config supports `loop`, `loopDelay`, `completeDelay`, `paused`, `persist`, and chain-level callbacks. Use `chain.add(tweenConfigs)` to append dynamically. - -Callbacks and events: -```js -this.tweens.add({ - targets: sprite, - x: 600, - duration: 2000, - // All callbacks receive (tween, targets, ...params) - onStart: function (tween, targets) { }, - onUpdate: function (tween, targets) { }, // per-property, per-target, per-frame - onComplete: function (tween, targets) { }, - onCompleteParams: ['extra', 'args'], - callbackScope: this -}); - -// Event emitter style (tweens extend EventEmitter) -tween.on('complete', function (tween, targets) { }); -``` - -Number tweens have no target — they tween between two numeric values: -```js -const counter = this.tweens.addCounter({ - from: 0, - to: 100, - duration: 2000, - ease: 'Linear', - onUpdate: function (tween) { - const value = tween.getValue(); // 0 ... 100 - } -}); -``` - -Controlling and killing tweens: -```js -const tween = this.tweens.add({ targets: sprite, x: 600, duration: 2000, persist: true }); -tween.pause(); tween.resume(); tween.stop(); tween.restart(); -tween.seek(1000); // seek to 1000ms (suppresses events by default) -tween.complete(); // immediately complete; fires onComplete -tween.updateTo('x', 800); // change end value mid-tween - -this.tweens.killAll(); -this.tweens.killTweensOf(sprite); -this.tweens.isTweening(sprite); -this.tweens.pauseAll(); this.tweens.resumeAll(); -this.tweens.setGlobalTimeScale(0.5); -``` -</common_patterns> - -<easing_functions> -Easing names are case-insensitive. Use the string name in the `ease` config property. - -Power aliases: `Power0` = `Linear`, `Power1` = `Quad.easeOut`, `Power2` = `Cubic.easeOut`, `Power3` = `Quart.easeOut`, `Power4` = `Quint.easeOut`. - -Each type supports `.easeIn`, `.easeOut`, `.easeInOut` variants; the bare name defaults to Out. -Types: `Quad`, `Cubic`, `Quart`, `Quint`, `Sine`, `Expo`, `Circ`, `Elastic`, `Back`, `Bounce`. -Special: `Linear` (no easing), `Stepped` (discrete steps — `easeParams: [numSteps]`). -Custom: `ease: function (t) { return t * t; }` where `t` is 0 to 1. -</easing_functions> - -<gotchas> -- Underscore properties are ignored. A tween skips any property whose name starts with `_`. -- Tweens auto-destroy. Using a stored reference after completion may fail. Set `persist: true` to keep it alive, then `destroy()` it yourself when done. -- `repeat` vs `loop`: `repeat` is per-property; `loop` restarts the entire tween. A `repeat` of 1 means the property plays twice total. -- `loop: -1` means `onComplete` never fires. Use `completeAfterLoop()` to end an infinite tween gracefully. -- Seeking suppresses events unless you pass `true` as the third argument to `tween.seek()`. -- Stagger only has a visible effect with multiple targets. -- Destroyed targets: a tween completes early if its target has `isDestroyed === true`. Clean up tweens before destroying objects they reference. -- Manager and tween time scales multiply: effective speed is `managerTimeScale * tweenTimeScale`. Either at 0 freezes the tween. -- Duration cannot be zero — it is clamped to a minimum of 0.01ms. -- TweenChain `targets` are inherited by child tweens unless a child specifies its own. -</gotchas> -</tweens> diff --git a/skills/base44-platform-games/references/threejs/assets_models_textures.md b/skills/base44-platform-games/references/threejs/assets_models_textures.md deleted file mode 100644 index 43efa13..0000000 --- a/skills/base44-platform-games/references/threejs/assets_models_textures.md +++ /dev/null @@ -1,25 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: assets models textures - -<assets_models_textures> -Use this when loading textures or 3D models, animating loaded models, or generating surfaces procedurally. - -<textures> -- `useTexture` is suspense-aware; the game image tools produce URLs that load straight into it. Keep generated URLs in `src/game/config.js`. -- Tiling a texture across a large ground plane: set `wrapS`/`wrapT` to `THREE.RepeatWrapping` and a `repeat` count. Pixel-art crispness: `magFilter = THREE.NearestFilter`. Color textures that look washed out: set `colorSpace = THREE.SRGBColorSpace` (the old `.encoding`/`sRGBEncoding` API is gone — never emit it). -- Procedural surfaces (stripes, grids, text-on-surface): draw a small offscreen 2D canvas once (memoized, never per frame) and wrap it in `THREE.CanvasTexture`. -</textures> - -<models> -- `useGLTF` loads a model (suspends); mount its `scene` with `<primitive object={...} />`. A loaded GLTF scene can be mounted only ONCE — render additional copies with drei `<Clone object={...} />`. -- Animations: `useAnimations(animations, groupRef)` exposes `actions`; crossfade states on gameplay events by fading out the current action and `reset().fadeIn().play()` on the next. Action names come from the file — log the keys of `actions` when unsure. -- Loaded models don't cast shadows by default: traverse the scene once and set `castShadow` on each mesh. -- Loaders cache by URL — using the same URL in ten components fetches once. Start downloads early with `useGLTF.preload`/`useTexture.preload` at module scope; drei `<Preload all />` inside the Canvas uploads everything to the GPU up front, killing first-encounter hitches. -</models> - -<sourcing> -Only reference model/texture URLs that actually exist (uploaded by the user or generated by the image tools) — there is no built-in asset library, and hallucinated URLs render nothing. When no model exists, build characters/props from primitives composed in a `<group>` (see the visuals guidance) — it looks intentional and ships now. -</sourcing> -</assets_models_textures> diff --git a/skills/base44-platform-games/references/threejs/cameras.md b/skills/base44-platform-games/references/threejs/cameras.md deleted file mode 100644 index fb62ae8..0000000 --- a/skills/base44-platform-games/references/threejs/cameras.md +++ /dev/null @@ -1,27 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: cameras - -<cameras> -Use this when setting up game cameras: follow/chase cameras, first-person, fixed/isometric angles, orbiting, and camera feedback (shake). - -<follow_camera> -- When the user didn't specify a camera style, default to a damped follow rig: a component whose `useFrame` eases `state.camera.position` toward an offset from the target each frame and then `lookAt`s the target. -- Use frame-rate-independent exponential smoothing — lerp with a factor of `1 - Math.exp(-k * delta)` (k ≈ 4; higher = snappier). A constant-factor lerp is a trap: it speeds up on high-refresh screens. -- The target position comes from the player RigidBody's `translation()` (or the mesh position in non-physics games); reuse one module-level scratch Vector3 rather than allocating per frame. -- Vary the offset for genre — top-down (0, 14, ~0) (the ~0 keeps the lookAt off dead-vertical, which would leave screen-up — and with it the key mapping — ambiguous), side-scroller (0, 3, 12) with a fixed lookAt height, isometric-style (8, 10, 8) with a lower fov (~35). Keep the offset in `src/game/config.js`. -- The offset either stays FIXED in world space (top-down, isometric, side-view, lane runner — the camera follows position only, never rotated) or rotates with the target's heading (chase rig — rotate the offset by the target's quaternion). In a chase rig, "behind" means a positive local-Z offset for a −Z-facing target, looking at a point ahead of it along −Z; left/right mirrored in play almost always means the camera is in FRONT of the facing — fix the offset sign. In a fixed rig, mirrored or 90°-rotated controls are a key→world mapping bug — fix the input mapping, not the camera. -</follow_camera> - -<control_helpers> -- `<OrbitControls makeDefault />` — inspect/orbit games (the template default). Constrain for gameplay: `minDistance`/`maxDistance`, `maxPolarAngle` just under PI/2 so the camera never goes below ground, `enablePan={false}`. In aim-and-power games the shot direction derives from the orbit camera's azimuth or the pointer hit point — one recorded source, like any other direction convention. -- `<PointerLockControls />` — first-person mouse look; the canvas captures the pointer on click, Esc releases. Pair with a capsule player body; show a "click to play" overlay hint while unlocked (`onLock`/`onUnlock`). -- A follow rig and OrbitControls fight each other — pick one. When mixing modes (orbit in menus, follow in play), gate them on the game phase. -- Cutscene-style pans and zoom-to-target: drei `<CameraControls makeDefault />` — its async API (`setLookAt` with the transition flag) animates smoothly. -</control_helpers> - -<feedback> -drei `<CameraShake>` adds impact feedback — mount it briefly on hits/explosions (small `maxYaw/maxPitch/maxRoll`, `decay` on), triggered by a short-lived state from an EventBus event. Small and brief reads as impact; constant shake reads as broken. -</feedback> -</cameras> diff --git a/skills/base44-platform-games/references/threejs/character_controller.md b/skills/base44-platform-games/references/threejs/character_controller.md deleted file mode 100644 index 1a0359a..0000000 --- a/skills/base44-platform-games/references/threejs/character_controller.md +++ /dev/null @@ -1,25 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: character controller - -<character_controller> -Use this when the player directly controls a walking/jumping character (platformer, third-person, first-person). This is the most failure-prone 3D pattern — follow these rules exactly. - -<capsule_recipe> -A dynamic RigidBody with `colliders={false}` plus an explicit `<CapsuleCollider>` (args are [halfHeight, radius]; capsules slide over small steps where boxes catch), driven by velocity in `useFrame`: -- `lockRotations` — physics must never tip the capsule over. Face the move direction by rotating the child MESH toward the velocity heading, not the body. -- Move with `setLinvel`, setting X/Z from input but PRESERVING the current Y velocity (read `linvel()` first) — overwriting Y kills gravity and jumping. -- Jump only when grounded: cast a short downward ray (`world.castRay` from `useRapier`) from just below the capsule base (~halfHeight + radius below the body's `translation()`); if it hits within ~0.2, set an upward Y velocity. -- `canSleep={false}` — a sleeping body ignores input until something wakes it. -- The wake flag (second argument `true`) on every `setLinvel`/`applyImpulse` call. -- Fall recovery: when `translation().y` drops below the world floor (e.g. -20), teleport back with `setTranslation` and zero the velocity. -- Camera-relative movement (rotating third-person camera): rotate the input vector by the camera yaw (from `camera.getWorldDirection`) before applying it. -</capsule_recipe> - -<ecctrl_fast_path> -For a full-featured third-person character (slopes, steps, moving platforms, camera follow modes) skip hand-rolling: install `ecctrl` with `npm install` — the official pmndrs floating-capsule controller built on this exact stack. Wrap the character model in `<Ecctrl>` inside `<Physics>`; it expects the drei `KeyboardControls` action names `forward/backward/leftward/rightward/jump/run`, and its `<EcctrlJoystick />` (rendered OUTSIDE the Canvas, before it) adds mobile touch controls automatically. - -Prefer ecctrl when the request is a real third-person game; prefer the capsule recipe for simple arcade movement or side-view platformers where its camera rig is unwanted. -</ecctrl_fast_path> -</character_controller> diff --git a/skills/base44-platform-games/references/threejs/enemies_and_movement.md b/skills/base44-platform-games/references/threejs/enemies_and_movement.md deleted file mode 100644 index 66f36a3..0000000 --- a/skills/base44-platform-games/references/threejs/enemies_and_movement.md +++ /dev/null @@ -1,31 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: enemies and movement - -<enemies_and_movement> -Use this when scripting non-player motion: chasing/patrolling enemies, moving platforms, doors/elevators, orbiting hazards, joints, and worlds that flow past the player. - -<world_flow> -When the world moves past a mostly-stationary player (rather than the player traversing a static world): -- Keep the player near the origin and move the world: spawn ahead at negative Z (the horizon), flow toward positive Z past the camera, cull a few units behind it. On screen the world flows from the horizon toward the bottom edge — ground sliding toward the horizon reads as moving backwards. -- Track bookkeeping stays naturally positive (distance run, segment index) — keep it positive, and convert to world space in exactly ONE shared formula that puts ahead at negative Z (`worldZ = traveled − trackZ`), used by EVERY flowing system (ground, obstacles, pickups, scenery). Dropping the negation because positive indices feel cleaner — or re-deriving the formula per file — is how one system ends up negated while the rest are correct and the scene tears in two directions. -- Ahead = negative relative Z, so visibility/cull windows run from −spawnDistance to a few units positive — a mirrored window makes objects invisible until they are already on top of the player. -</world_flow> - -<kinematic_movement> -- Script-driven movers are `type="kinematicPosition"` bodies advanced each frame with `setNextKinematicTranslation` — they push/carry dynamic bodies (including the player) but nothing pushes them back. Scale all motion by `delta`. -- Chase: each frame, take the direction from the enemy's `translation()` to the player's on the ground plane (zero the Y component), normalize, scale by speed × delta, and set the next translation. Stop advancing inside melee range and attack instead. Reuse one module-level scratch Vector3 — no per-frame allocation. -- Turret targeting, AoE, nearest-enemy scans: iterate the shared live registry (`world.enemies` — see game_state_and_flow state_home) inside `useFrame`; enemies register on spawn, deregister on death. Never scan a useState array. -- Patrol, platforms, elevators, spinners: drive position from `state.clock.elapsedTime` (sine/cosine of it) rather than accumulating your own offsets — deterministic, no drift. Rotating movers use `setNextKinematicRotation` with a quaternion. -- Simple AI state machines (patrol → chase when within aggro radius → attack in melee range) are a phase field in a ref checked at the top of `useFrame` — no library needed. -</kinematic_movement> - -<dynamic_enemies> -Enemies that should be shoved by explosions/hits are dynamic capsules like the player (lockRotations, velocity steering via `setLinvel` toward the target instead of kinematic translation). Kinematic = reliable pursuit that cannot be body-blocked; dynamic = physical comedy and knockback. Pick per game feel. -</dynamic_enemies> - -<joints> -Joints connect two RigidBody refs: `useRevoluteJoint` (hinge — doors, flippers), `useSphericalJoint` (ball-socket — swinging ropes, a wrecking ball on a fixed anchor), `useFixedJoint`, `usePrismaticJoint` (sliders). Chain segments by joining successive bodies. Reach for joints only when the physics should visibly articulate — most gameplay motion is the kinematic scripting above. -</joints> -</enemies_and_movement> diff --git a/skills/base44-platform-games/references/threejs/game_state_and_flow.md b/skills/base44-platform-games/references/threejs/game_state_and_flow.md deleted file mode 100644 index 8038957..0000000 --- a/skills/base44-platform-games/references/threejs/game_state_and_flow.md +++ /dev/null @@ -1,36 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: game state and flow - -<game_state_and_flow> -Use this when structuring game phases (menu → playing → game over), score/health state, pause, restart, and saving progress. - -<state_home> -Two kinds of state, two homes: -- Discrete/UI state (phase, score, lives, unlocks) — React state in the overlay, updated via EventBus events from gameplay. Low frequency, drives DOM. -- Per-frame gameplay state — refs and module objects read inside `useFrame`. This covers scalars (positions, velocities, timers, mid-run combo counters) AND entity collections with their mutable fields: the live arrays of enemies/towers/projectiles and each entity's hp/target/cooldown. Never React state — beyond re-render stutter, `useFrame` callbacks capture the array from the render that created them, so a tower reading a useState `enemies` list targets a STALE snapshot: damage lands on ghosts and kills never register. Writing game logic as `setState(prev => ...)` updaters (or nesting one setState inside another) is the same failure. -- When state must be BOTH (read every frame in gameplay AND shown reactively in UI), install `zustand` with `npm install`: components subscribe with the hook selector, `useFrame` reads via `getState()` without re-rendering. For simple games, EventBus + a module object is enough — don't add zustand for a single score counter. -- Systems that iterate many entities each frame share ONE live registry, mutated in place: -```js -// src/game/3d/world.js — module scope (or one useRef/context at the scene root) -export const world = { enemies: [], towers: [], projectiles: [] }; -``` -Spawn = push, death = splice or flag-and-reuse (see projectiles_and_pooling); each system's `useFrame` (targeting, projectiles, wave spawner) reads `world.enemies` live — no props, no copies. Entries hold plain fields plus refs to their body/mesh. The HUD never reads the registry: gameplay emits discrete or throttled (100-250ms) EventBus/zustand snapshots and React renders that copy. On restart, empty the arrays explicitly — the `key={runId}` remount does not clear module scope. -</state_home> - -<phases> -- Gate on the phase: the `paused` prop on `<Physics>` when not playing, conditional-render the level subtree, and early-return guards at the top of gameplay `useFrame` callbacks. -- The Canvas stays mounted through every phase — menus render as overlay on top of an idle scene, which also looks better than an empty canvas. -- Restart: remount the play-world subtree with a key (`key={runId}`, incremented on restart). Remounting resets every body, ref, and timer to initial JSX state in one move; manual per-object reset code is a bug farm. This is the ONE place mass unmount/remount is correct — once per run, not per frame. -- UI countdowns and run timers default to the overlay (interval or elapsed-from-start-timestamp); a timer that gameplay itself consumes (physics-coupled time limits, slow-motion) belongs in the loop/physics step instead. -</phases> - -<persistence> -Only persistence path: define a Base44 entity schema (for example `base44/entities/HighScore.jsonc`) and read/write it with the SDK (`base44.entities.*`) from React overlay/page code. -- Do not persist game data in `localStorage`, auth profile fields, refs, module globals, config files, or Three.js/R3F state. -- Gameplay emits final/progress data through `EventBus` at phase boundaries; React calls `base44.entities.<Entity>.create/filter/update/delete`. -- React passes loaded entity data back into gameplay as runtime state. Never call the SDK inside `useFrame`, physics callbacks, or render-loop systems. -- Leaderboards use open `read` and owner-scoped writes; never pass owner fields manually. -</persistence> -</game_state_and_flow> diff --git a/skills/base44-platform-games/references/threejs/input_and_controls.md b/skills/base44-platform-games/references/threejs/input_and_controls.md deleted file mode 100644 index f278e0f..0000000 --- a/skills/base44-platform-games/references/threejs/input_and_controls.md +++ /dev/null @@ -1,28 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: input and controls - -<input_and_controls> -Use this when handling player input: keyboard movement/actions, clicking or hovering 3D objects, and touch controls on mobile. - -<keyboard> -- Map physical keys to named actions once with drei `<KeyboardControls map={...}>` wrapping the scene root inside the Canvas — actions like forward/back/left/right/jump, each listing its key codes (`KeyW`, `ArrowUp`, `Space`, ...). -- For per-frame input, use the imperative tuple form exactly: `const [, getKeys] = useKeyboardControls();`, then inside `useFrame` read fresh state with `const keys = getKeys();`. This does NOT re-render. Convert the booleans to a -1/0/1 axis and scale movement by `delta` (or feed the physics API). -- The selector form returns the selected value directly: `const jump = useKeyboardControls((s) => s.jump);` makes `jump` a boolean and re-renders on change, so use it only for discrete UI-ish reactions, never for movement. NEVER call a selector result — `const getKeys = useKeyboardControls((s) => s); getKeys()` is invalid because `getKeys` is a state object, not a function. -- Apply axis signs in ONE place, derived from the convention recorded in config. The trap is double negation: an axis computed `left − right` and then negated again downstream "for feel" leaves A steering right. -- Two control models, picked per game and independent of the camera: DIRECT ( twin-stick, platformer — a key means a direction) and STEERED (car, plane, boat — A/D turn the heading, W/S throttle along it, under ANY camera including top-down). After wiring, trace one key end-to-end: direct — D must move toward screen-right as the player sees it (a fixed world direction under a fixed camera; rotated by the camera yaw under a rotating chase/first-person camera); steered — D must turn the vehicle toward ITS OWN right (clockwise seen from above; increasing `rotation.y` turns LEFT), even when it faces the camera and that reads as screen-left. -- Match the scheme to the vehicle when the user didn't specify: aircraft and ships don't reverse — W/S are throttle up/down (floored at zero or a stall speed), not forward/backward; cars reverse; runners steer lanes and never move backwards. Under direct control, rotate the avatar MESH to face its velocity (or the aim, in twin-stick) — the keys move the body, not the facing. -</keyboard> - -<pointer> -- Meshes take pointer events directly (`onClick`, `onPointerOver/Out/Down`) — R3F raycasts for you; no manual Raycaster. `e.stopPropagation()` blocks hits on objects behind; `e.point` is the world-space hit position — the standard way to aim or place things by click (click-to-move on a ground plane is just an `onClick` on it). -- Set `document.body.style.cursor` in `onPointerOver`/`Out` for hover affordance. -</pointer> - -<touch> -- If the user didn't specify mobile controls, a DOM joystick in the React overlay is a good default — but match the genre: tap-to-move/tap targets suit point-and-click and board games, swipe suits runners. -- Wire touch input through a shared module object (e.g. `src/game/input.js` exporting a mutable state the overlay writes on touch events and gameplay reads in `useFrame`) — never React state per move event. -- Keep touch targets comfortably large (~64px). Show touch controls only on coarse pointers: `window.matchMedia('(pointer: coarse)').matches`. -</touch> -</input_and_controls> diff --git a/skills/base44-platform-games/references/threejs/performance.md b/skills/base44-platform-games/references/threejs/performance.md deleted file mode 100644 index 607a091..0000000 --- a/skills/base44-platform-games/references/threejs/performance.md +++ /dev/null @@ -1,33 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: performance - -<performance> -Use this when a scene has many objects, when frame rate matters (it always does in games), or when reviewing gameplay code for jank. - -<the_rules> -1. No React state changes per frame — mutate refs in `useFrame`; React state only for discrete events. This is the #1 3D-game jank source, and for entity collections (enemy/projectile arrays, per-entity hp) it is also a correctness bug: closures read stale snapshots (see game_state_and_flow). -2. No allocations in hot loops — a `new` Vector3 per frame churns the GC into stutters. Hoist scratch objects to module scope (or `useMemo`) and reuse them via `set(...)`. -3. Share geometries and materials. Inline geometry/material JSX per mesh creates one of each per mount — fine for a dozen meshes, deadly for hundreds. Create them once (module scope or memoized) and pass via the `geometry`/`material` props. -4. Scale movement by `delta` — code tuned at 60fps without delta runs double speed on 120Hz screens. -</the_rules> - -<many_objects> -- Each mesh is a draw call; hundreds of identical objects (trees, coins, tiles, debris) belong in ONE instanced draw call — drei `<Instances>` (one geometry + material) with an `<Instance>` per item, each still taking position/rotation/scale/color and pointer events. Set `limit` to the maximum count. -- The physics twin is `InstancedRigidBodies` (an `instances` array prop wrapping one `instancedMesh`) for piles of physical debris. -- Distant detail: drei `<Detailed>` with per-distance children swaps LOD levels automatically. -</many_objects> - -<measuring_and_degrading> -- drei `<StatsGl />` while developing when performance is in question; judge draw calls and fps, then remove it. -- Auto-degrade on weak devices with `<AdaptiveDpr>`/`<PerformanceMonitor>` (see scene_and_rendering). -- Raycast-heavy scenes (constant pointer hover over complex meshes) get free speed from wrapping the scene in drei `<Bvh>`. -- Shadow maps are the silent budget-eater: a single 2048 map is usually enough; prefer turning `castShadow` off on small clutter over growing the map. -</measuring_and_degrading> - -<react_shape> -Keep the component tree stable during play: conditional-render UI states, but never map gameplay entities off arrays that change every frame, and never key things by per-frame values — both force remounts (see the pooling rule in projectiles_and_pooling). If a component re-renders every frame, something reads per-frame state reactively; move that read into `useFrame`/`getState()`. -Debugging tells that gameplay data lives in useState: damage that never applies, turrets targeting already-dead enemies, `setState(prev => ...)` updaters as game logic (or nested inside other updaters). The fix is always the same — move the collection to a ref/module registry and keep React as a throttled HUD mirror. -</react_shape> -</performance> diff --git a/skills/base44-platform-games/references/threejs/physics_dynamics.md b/skills/base44-platform-games/references/threejs/physics_dynamics.md deleted file mode 100644 index 69d74ba..0000000 --- a/skills/base44-platform-games/references/threejs/physics_dynamics.md +++ /dev/null @@ -1,26 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: physics dynamics - -<physics_dynamics> -Use this when configuring rigid bodies, colliders, and forces with @react-three/rapier — stacking, bouncing, knockback, vehicles, physical props. - -<bodies_and_colliders> -- Body types: `type="fixed"` for level geometry, dynamic (the default) for anything gravity and forces should move, `type="kinematicPosition"` for code-driven movers (it pushes dynamic bodies but is immovable itself). -- Collider auto-generation from child meshes via the `colliders` prop: `"cuboid"` (default) | `"ball"` | `"hull"` (convex wrap — the right choice for most dynamic props) | `"trimesh"`. HARD RULE: `trimesh` only on `fixed` bodies (concave level geometry) — a dynamic trimesh is slow and tunnels. -- Compound or offset shapes: `colliders={false}` plus explicit `<CuboidCollider>`/`<BallCollider>`/`<CapsuleCollider>`/`<CylinderCollider>` children. Explicit collider `args` are HALF-extents. -- Material feel per body/collider: `restitution` (bounciness), `friction`, `mass` (or `density`). `ccd` on small fast bodies stops them tunneling through walls. The `debug` prop on `<Physics>` renders all colliders — dev only. -</bodies_and_colliders> - -<moving_bodies> -- Never mutate mesh positions inside a RigidBody and never move a dynamic body via props after mount — always the body ref API: `applyImpulse` (instant kick — jumps, hits, explosions), `setLinvel` (direct control), `applyTorqueImpulse`/`setAngvel` (spin), `setTranslation` (teleport/respawn — also zero the velocity). -- Every one of those calls takes a wake flag as its last argument — pass `true`; sleeping bodies ignore forces. -- Impulses scale with mass: the same impulse that launches a mass-1 crate nudges a mass-10 one. Read state with `translation()`, `linvel()`, `rotation()`. -- Common tuning: floaty jumps → stronger gravity (around -20) or extra downward force while airborne; jittery stacks → bigger colliders and more mass; everything slides → raise `friction` on both surfaces. -</moving_bodies> - -<per_step_logic> -Physics steps at a fixed 1/60 timestep decoupled from rendering. Logic that must see every step (custom gravity, force fields, speed clamps) belongs in `useBeforePhysicsStep`, not `useFrame`. Pause the whole simulation with the `paused` prop on `<Physics>`. -</per_step_logic> -</physics_dynamics> diff --git a/skills/base44-platform-games/references/threejs/projectiles_and_pooling.md b/skills/base44-platform-games/references/threejs/projectiles_and_pooling.md deleted file mode 100644 index 24b0f8e..0000000 --- a/skills/base44-platform-games/references/threejs/projectiles_and_pooling.md +++ /dev/null @@ -1,25 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: projectiles and pooling - -<projectiles_and_pooling> -Use this when things spawn and despawn rapidly: bullets, thrown objects, falling debris, wave spawns. The core rule: NEVER mount/unmount React components per spawn — pool them. - -<pool_pattern> -- Mount a fixed pool of RigidBodies once (an array render with stable keys, refs collected into a ref array). "Spawning" teleports a parked body in (`setTranslation`) and gives it velocity (`setLinvel`); "despawning" parks it again. -- Park inactive bodies far below the level (staggered positions so they don't overlap), with `gravityScale={0}` and zeroed velocity so they stay put; restore gravityScale on activation if the object should fall. -- Recycle round-robin: keep a next-slot index; the oldest active body is simply reused when the pool wraps. -- Despawn on contact via `onCollisionEnter` (apply the hit, then park the payload's target body) AND by lifetime — a `useFrame` sweep parking anything active older than a few seconds (spawn times in a ref array). -- `ccd` on bullets stops fast ones tunneling through thin walls. -- The same pattern serves enemies, debris, and floating damage numbers. -</pool_pattern> - -<hitscan> -- Instant-hit weapons and line-of-sight checks skip projectile bodies entirely — cast a ray through the physics world: `world.castRay` from `useRapier`, constructing the ray with the rapier `Ray` class from an origin and direction. -- The hit result's field is `timeOfImpact` (the old name `toi` was renamed — do not emit it); get the world-space point via the ray's `pointAt(timeOfImpact)`, and identify what was hit through the hit collider's parent body (name/userData). -- Exclude the shooter from its own ray via the cast's filter argument (pass the shooter's rigid body as the excluded body). -- Aim from the camera with `camera.getWorldDirection`. -- Decision rule: hitscan + a pooled tracer/impact effect for anything faster than the eye; pooled bodies when travel time and arcs are part of the gameplay. -</hitscan> -</projectiles_and_pooling> diff --git a/skills/base44-platform-games/references/threejs/scene_and_rendering.md b/skills/base44-platform-games/references/threejs/scene_and_rendering.md deleted file mode 100644 index eed0c08..0000000 --- a/skills/base44-platform-games/references/threejs/scene_and_rendering.md +++ /dev/null @@ -1,29 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: scene and rendering - -<scene_and_rendering> -Use this when configuring the Canvas, lights, shadows, atmosphere (sky, fog, background), or render quality for a 3D game. - -<canvas_and_lights> -- One persistent `<Canvas>` in `src/game/3d/Game.jsx` (never remount it), configured via props: `shadows`, `dpr={[1, 2]}`, and the default `camera` (fov/near/far/position). One `<Suspense fallback={null}>` at the scene root covers all async content (`<Physics>`, textures, models). -- Light the scene so it reads clearly by default — unless the user asks for a dark, moody, or horror look. Give it a motivated key light chosen for the space: a sun-like `directionalLight` for open/outdoor scenes, a `spotLight` or `pointLight` for indoor or localized pools — positioned and powered to actually illuminate the play area and cast readable shadows. -- Add an `ambientLight` or `hemisphereLight` (sky/ground colors) as gentle fill ONLY — enough to keep shadowed faces off pure black, never to light the scene (fill is flat and shadowless). A hemisphere fill reads better than a second directional. -- A scene that renders dark almost always means the key is too weak or aimed away from the action, or the palette itself is near-black (see render_quality) — fix the key or the albedo rather than piling on ambient or cranking exposure. -- Shadows silently missing means one of three flags is absent — Canvas `shadows`, `castShadow` on the caster, `receiveShadow` on the receiver — or the play area outgrew the directional light's shadow camera box (`shadow-camera-left/right/top/bottom` must cover it). Bump `shadow-mapSize` for crisper edges; a small negative `shadow-bias` fixes striped self-shadowing ("shadow acne"). -</canvas_and_lights> - -<atmosphere> -- Set the scene backdrop with `<color attach="background" />` and add `<fog attach="fog" />` matched to the background color — fog is the cheapest depth cue and hides the far clip. -- Fog is a depth cue, not a curtain: start the fog band beyond the playfield at the camera's typical distance — a near band matched to a dark background fades the whole play area toward black. -- drei quick picks: `<Sky>` (procedural daytime sky), `<Stars>` (night/space). `<Environment preset>` adds image-based lighting BUT downloads its HDRI from a CDN at runtime — prefer `<Sky>` + lights; use Environment only when realistic reflections matter and the network dependency is acceptable. -</atmosphere> - -<render_quality> -- drei `<AdaptiveDpr pixelated />` drops resolution automatically while the frame rate is low; `<PerformanceMonitor onDecline/onIncline>` can drive coarser quality toggles (shadow size, effect density). -- Color management is automatic in current three.js; R3F defaults to ACESFilmic tone mapping at exposure 1. `toneMappingExposure` (set via the Canvas `gl` prop) is the master brightness dial — a small nudge shifts overall mood, but it is not a substitute for lighting the scene; fix darkness at the light, not the exposure. -- Lighting cannot rescue near-black materials: give large surfaces (ground, boards, walls) mid-tone or brighter albedo by default — unless the user asks for a dark or night look — and save near-black for accents. -- Metallic/reflective materials need an environment to reflect or they render black — keep default `meshStandardMaterial` low-`metalness` unless an `<Environment>` or bright surroundings are present. -</render_quality> -</scene_and_rendering> diff --git a/skills/base44-platform-games/references/threejs/text_hud_ui.md b/skills/base44-platform-games/references/threejs/text_hud_ui.md deleted file mode 100644 index cf530a7..0000000 --- a/skills/base44-platform-games/references/threejs/text_hud_ui.md +++ /dev/null @@ -1,29 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: text hud ui - -<text_hud_ui> -Use this when showing text or UI: world-anchored labels, floating damage numbers, health bars above heads — and the boundary with the React overlay. - -<the_boundary> -This section covers text/UI attached to points in the world (name tags, damage numbers, signs, health bars over enemies); screen-space UI belongs in the React overlay per the architecture instructions — never as meshes, never via drei. When in doubt: no world position → overlay. -</the_boundary> - -<world_text> -- drei `<Text>` renders crisp SDF text at any size and angle (not blurry canvas sprites) — `fontSize`, `color`, `anchorX/anchorY`, and `outlineWidth`/`outlineColor` for readability against the scene. -- Wrap in `<Billboard>` to always face the camera (name tags, indicators). -- 3D extruded `<Text3D>` needs a bundled font JSON — prefer flat `<Text>` unless the text IS the centerpiece. -</world_text> - -<damage_numbers> -Floating combat text is a pooled set of `<Text>` items animated in `useFrame` (rise + fade via the text material's opacity, with `transparent` on), activated from an EventBus event carrying a position — same pooling rule as projectiles: never mount/unmount per hit; park when invisible. -</damage_numbers> - -<world_html> -- drei `<Html>` places real DOM at a 3D point — the pragmatic choice for styled micro-UI like health bars (a Tailwind-styled div). `center` anchors the middle, `distanceFactor` scales it with distance so it behaves like a world object, `occlude` hides it behind geometry. -- Update it imperatively (a ref setting styles) from events — not React state per frame. -- Each Html instance is a live DOM element — fine for a handful (bosses, nearby enemies), wrong for hundreds; beyond that use a thin mesh scaled by value instead. -- R3F hooks don't work inside the Html subtree (it renders to the DOM, outside the Canvas reconciler). -</world_html> -</text_hud_ui> diff --git a/skills/base44-platform-games/references/threejs/triggers_and_collisions.md b/skills/base44-platform-games/references/threejs/triggers_and_collisions.md deleted file mode 100644 index 7fe3cfc..0000000 --- a/skills/base44-platform-games/references/threejs/triggers_and_collisions.md +++ /dev/null @@ -1,28 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: triggers and collisions - -<triggers_and_collisions> -Use this when gameplay reacts to contact: pickups, damage zones, goals, checkpoints, projectile hits, "what did I touch?". - -<identifying_bodies> -Give bodies a `name` (exposed on the event payload's three object) or `userData` for richer typing — this is how handlers know WHAT was hit: in the event payload, check `other.rigidBodyObject?.name` or `other.rigidBody?.userData`. -</identifying_bodies> - -<sensors> -- A collider with the `sensor` prop detects overlap but has no physical presence — THE gameplay trigger primitive (coins, kill zones, goal lines, checkpoints, aggro ranges). Sensors fire `onIntersectionEnter` / `onIntersectionExit` (solid colliders do not). -- The standard pickup: a `fixed` RigidBody with `colliders={false}`, an explicit sensor collider, and an `onIntersectionEnter` that checks the other body's name is the player, then emits an EventBus event (score/HUD updates on the React side) and hides the item via React state — a discrete event, so state is fine here. -- A large sensor box under the level is the standard fall-death detector. -</sensors> - -<solid_collisions> -- Solid bodies fire `onCollisionEnter` / `onCollisionExit` (on the RigidBody or a specific collider). Check who hit via the payload's name/userData as above. -- Debounce repeated contacts — an enemy grinding against the player fires enter/exit rapidly; track the last-hit time in a ref and ignore hits inside the invulnerability window. -- `onContactForce` reports impact magnitude — for "break on hard landing" style rules. -</solid_collisions> - -<filtering> -`collisionGroups` with the `interactionGroups(group, [collidesWith...])` helper (imported from @react-three/rapier) when categories must pass through each other — e.g. enemy projectiles ignoring enemies. Groups 0-15. Prefer plain name checks in handlers until pass-through behavior is actually required. -</filtering> -</triggers_and_collisions> diff --git a/skills/base44-platform-games/references/threejs/vfx_and_juice.md b/skills/base44-platform-games/references/threejs/vfx_and_juice.md deleted file mode 100644 index 049411e..0000000 --- a/skills/base44-platform-games/references/threejs/vfx_and_juice.md +++ /dev/null @@ -1,36 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 games: Three.js: vfx and juice - -<vfx_and_juice> -Use this when adding feedback and polish ("juice"): particles, trails, glows, hit flashes, audio, win-moments. When the user hasn't specified polish, a light juice pass is usually what makes a simple game feel finished — suggest it rather than skipping it silently. - -<drei_effects> -- `<Sparkles>` — ambient magic/pickup shimmer. -- `<Trail>` — motion readability on anything fast; it follows the mesh rendered as its child. -- `<Outlines>` — selection/highlight, rendered inside a mesh. -- One-shot bursts (explosion, pickup pop): a pooled `<Sparkles>` repositioned to the event point for ~0.5s — pooled and reused like projectiles, never created per event. -</drei_effects> - -<hit_feedback> -A menu to pick from — each is a few lines; when the user didn't specify hit feedback, one or two of these on significant hits is a good default: -- Camera shake: brief `<CameraShake>` (see the cameras section). -- Flash: swap the target's material color/emissive for ~100ms (ref + timeout restoring the original). -- Scale pop: damp the mesh scale up to ~1.3 and back in `useFrame`. -- Hit-stop: freeze physics a beat — the `paused` prop on `<Physics>` for 60-100ms on big hits. -- Floating damage number (see text_hud_ui). -</hit_feedback> - -<audio> -drei `<PositionalAudio>` attaches 3D sound to an object (`loop` for ambience; call `play()` on its ref for one-shots). Browsers block audio until a user gesture — start playback only after the first click/keydown (the start-screen "Play" button is the natural unlock). Screen-space UI sounds can be a plain Audio element on the overlay side. Only reference audio URLs that actually exist (user-uploaded or provided) — there is no built-in sound library; when none exist, skip audio rather than pointing at dead URLs. -</audio> - -<win_moments> -`canvas-confetti` is already a dependency — fire it from the React overlay on win/level-complete EventBus events. If the user didn't specify a win moment, confetti + a camera move toward the goal (drei CameraControls transition) + a scale-pop on the goal object is a combo that works well. -</win_moments> - -<postprocessing> -For bloom (glowing projectiles/neon), vignette, or chromatic aberration, install `@react-three/postprocessing` with `npm install`: one `<EffectComposer>` as the last child inside the Canvas, with effect components inside (all effects in the composer merge into a single pass). For selective glow, set `luminanceThreshold` to 1 on `<Bloom>` (with `mipmapBlur`) so only overbright objects bloom, and make materials overbright via `emissive` + `emissiveIntensity` above 1 (or `toneMapped={false}` on plain colors). Skip postprocessing on visibly low-end targets (pair with PerformanceMonitor). -</postprocessing> -</vfx_and_juice> diff --git a/skills/base44-platform-integrations-sdk/SKILL.md b/skills/base44-platform-integrations-sdk/SKILL.md index e31937c..5d2986a 100644 --- a/skills/base44-platform-integrations-sdk/SKILL.md +++ b/skills/base44-platform-integrations-sdk/SKILL.md @@ -15,7 +15,7 @@ Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (ov tool, or with your Base44 CLI token); it returns the framework, app type, runtime, entitlements and the exact reference files to read. Without it, detect from the project files: `src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a -native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. +native mobile (Expo) app; otherwise it is a Vite SPA. | Profile | Read | |---|---| diff --git a/skills/base44-platform-manifest.json b/skills/base44-platform-manifest.json index 04e45dd..9864136 100644 --- a/skills/base44-platform-manifest.json +++ b/skills/base44-platform-manifest.json @@ -1,80 +1,36 @@ { "files": { - "skills/base44-platform-actors/SKILL.md": "c3844ddcf866e7c4f1adb922a7fec64c0fd44427d9356f4ea104b08f031a9f13", + "skills/base44-platform-actors/SKILL.md": "52d3c0959e896cbbaeff58ce43c28687bef630de1c164dafb21d6dcab3086321", "skills/base44-platform-actors/references/mobile.md": "31cc9bf4dcb7aacc9e8f98df8020c86130d0c14f719518945d6613d29798dbfe", "skills/base44-platform-actors/references/web.md": "de7d5e7aea53b8a9e1f4eb89a75298d49a72da1b923215e87779926c6b7cf655", - "skills/base44-platform-agents/SKILL.md": "c791b870ff47eeafedcf2604e42b57b823043856d23d1cf657fb2198333cdb5f", + "skills/base44-platform-agents/SKILL.md": "1d6f55d6bd71449d629b2662c647e8e242bdb0ab6432c33d34534437241888ab", "skills/base44-platform-agents/references/web-spa.md": "d429d967bd32996e597ce1386a8d255972f79080545536b8e9a5bca2322048c0", "skills/base44-platform-agents/references/web-tanstack.md": "e48e0c5e2373ddcad47c02722b4b5dc5971640536b30e99896ef5113f97f4d74", - "skills/base44-platform-backend-functions/SKILL.md": "0b1034eb6965a723fd10b5b70fa4616e2297938087a019fc8fce30d10e27f794", + "skills/base44-platform-backend-functions/SKILL.md": "146505a402ba4ba478f4042dfbf2e6d46e3497fa73e9102ab8404c76224a3376", "skills/base44-platform-backend-functions/references/mobile.md": "034fe28f812897363b91242078a1092943e319fdee0fc28ea8d2fd779deb8498", "skills/base44-platform-backend-functions/references/web-spa.md": "43620240a07f0e0b776ebb7c38945fddecd806a23a963b7b3ec5fd2feeecbfaa", "skills/base44-platform-backend-functions/references/web-tanstack.md": "960f4fede0d0c878e8e98b23c196ac9fcc99a7b9e2ef8f46cf005d607e73f0cd", - "skills/base44-platform-connectors/SKILL.md": "278579146b5e422f11cb15a9b33e5955cd9cbb62f0791c4cd40ca49535492a51", + "skills/base44-platform-connectors/SKILL.md": "e037d9536b1d8267b85a7d25ab0416c1a8fb0f88763e2365ebae97cec23a4bea", "skills/base44-platform-connectors/references/mobile.md": "7cb1dbad4829a203a0dc44bde8646410e3484c9fb45c64de0f7201d870e5aefd", "skills/base44-platform-connectors/references/web-spa.md": "aa905eab8a95f97c85e4755123eae2b38c5e4e62a09c516d481519b51d04734c", "skills/base44-platform-connectors/references/web-tanstack.md": "cec2fb0c272335ccb605f774bd3f8b621b37d3f8985c1e9ff4c56857a3d6b209", - "skills/base44-platform-entities/SKILL.md": "5e40879dbb791b271953e8decc5b80ed63e950668b9522b65b45eb9c3647ad62", - "skills/base44-platform-entities/references/mobile-and-games.md": "180accec4af5547da524e09fed6ed36414ea3d64f24cf622e40654a51565bb8a", + "skills/base44-platform-entities/SKILL.md": "9b37a2aff19212a88aa0c14b113a3246c9ae714a8b45247f0c2e7c251d0c9466", + "skills/base44-platform-entities/references/mobile.md": "cbee462933515db5329f9901ff401d666f9084b7bb1a5ca91c90be6b33101240", "skills/base44-platform-entities/references/rls.md": "a87a71751bb63c28a0629c2ca65019c726c3b549149a029f74f5a84cb86b8dcf", "skills/base44-platform-entities/references/web.md": "e30672b094b3b5ed2fc926b06ee3646a8f1777491cae41cfb9a0be65f8bb1a9b", - "skills/base44-platform-frontend/SKILL.md": "cfb5241785f6f0b6cba3cf1c6206849e19e20a9f46d1851a37afe840cbd90e59", + "skills/base44-platform-frontend/SKILL.md": "0b06d58d67108e260d147d1ced4143755a451823e59e6a3012cfd71556921ac7", "skills/base44-platform-frontend/references/mobile.md": "ecf9499821d8cf345e8b32143f2d0f3e72a4daeda06588a251d26f546bcac60a", "skills/base44-platform-frontend/references/web-spa.md": "410aee9583f1cf24dead07d4429d27ca7063e38053d50aa1cf7bc2a88305c56b", "skills/base44-platform-frontend/references/web-tanstack.md": "5c4227b1fc2fa80f2da40dff60e68f67f8c0054cdbf7758bc43bb6cea49d6614", - "skills/base44-platform-games/SKILL.md": "4338b805e5e066045e015d26d5c4bb9aacfb077aa828095f723608b11662abbf", - "skills/base44-platform-games/references/game-2d.md": "2026065231813e8420e26bbdcd96928e443141fd49cbf006ca064a4f8e7eb006", - "skills/base44-platform-games/references/game-3d.md": "782a89cb6cab7355b5e2016cb401e0f0966082853b5a96fb5ff876807d5eee39", - "skills/base44-platform-games/references/phaser/actions_and_utilities.md": "66e7414bbb95280f91bd36dd933a578bbf6dfe8e6de284a17de774bd3da1c759", - "skills/base44-platform-games/references/phaser/animations.md": "4ad18783e90621a36e4dc059cbed212a5e8159a1b09b41db8b157a34e481966f", - "skills/base44-platform-games/references/phaser/audio_and_sound.md": "c3d8d1b1a0e201bbd5e181159755e7c97355694fe1dfcf566687cd660c7ab72c", - "skills/base44-platform-games/references/phaser/cameras.md": "e118aecc47ab3117187b2e66aea784a37ad1031a9ed46f23bfbbeb71e4fa4542", - "skills/base44-platform-games/references/phaser/curves_and_paths.md": "057362007b900aa9afed476821cd5d33336810d08f891b1403ad3ef0f004490c", - "skills/base44-platform-games/references/phaser/data_manager.md": "cbc11427efa8e8165638c23a09e62b4e2df0d106b41f9e1c8b5ee79a34ee7784", - "skills/base44-platform-games/references/phaser/events_system.md": "8ed1067d88b7a3076bcee9b6d9fbf38e5b8993b0ee02f7110c92fe03050049bf", - "skills/base44-platform-games/references/phaser/filters_and_postfx.md": "0f99a61b7ea509bf98bf44019ee03977bc98b93feb196fb935cd00b84467261e", - "skills/base44-platform-games/references/phaser/game_object_components.md": "f339f7314e2aeb2d5f044123c34f972e4101b31e83bdd06b3089ceefaaf0112e", - "skills/base44-platform-games/references/phaser/game_setup_and_config.md": "5dcf957a43ac39c380f41eea628ae9e57c659a418fb38c1535723d99b8f8d354", - "skills/base44-platform-games/references/phaser/geometry_and_math.md": "1ebbe11154252933a2744c47366c3c1f08a4bf57589ad57cb666f4448977f74d", - "skills/base44-platform-games/references/phaser/graphics_and_shapes.md": "5149f3dfc8a22c8be01f1564e913e54dacb56a460e66d4cc89d386a71c0bbcd3", - "skills/base44-platform-games/references/phaser/groups_and_containers.md": "61e1d27f415612bf3475e0bf8f49f2bb87b9ae22ad67eedf80c0f9814d38cc21", - "skills/base44-platform-games/references/phaser/input_keyboard_mouse_touch.md": "ff3feb37602af540a4626dcdd12aebabe6679aa4ce629715aa4d4bcdb95088f3", - "skills/base44-platform-games/references/phaser/loading_assets.md": "c1780741e20f125e322cdb547c2ed05425d64d3ca2cefdd44bd8b6789cc24f16", - "skills/base44-platform-games/references/phaser/mesh_and_3d_elements.md": "7522787a0513f6db0dcdb03e113ca85898a0dec694f33e82abce998aa65fe8e1", - "skills/base44-platform-games/references/phaser/particles.md": "7004c2c67f88a8cb1719b3e24abfff03a0550527be24ad119256fe2d206c14cf", - "skills/base44-platform-games/references/phaser/physics_arcade.md": "73efe5bf13739dc759a3c277c3064716cb2224740b4f0b8c2c036b88eceb0862", - "skills/base44-platform-games/references/phaser/physics_matter.md": "620b1a18794e1650d5b2580ea212caddc379da2665549b05da18fe9e9242236c", - "skills/base44-platform-games/references/phaser/render_textures.md": "d7422316735a8cedde9712850ba13e6a90369aa20d144fe0531872e261925948", - "skills/base44-platform-games/references/phaser/scale_and_responsive.md": "1ff70ea1cf861329a71f70bc913790dae332cb2e47d83ade2e3e08e3def54c9e", - "skills/base44-platform-games/references/phaser/scenes.md": "09ef9a0a4f91955f86defc5b22fbf7cfd77ae01797c035a345ab85d72719fdca", - "skills/base44-platform-games/references/phaser/sprites_and_images.md": "9f063ee60fae8ae057c1034c8503e9ed813f82e91c9e296dec2245e0670dc5a0", - "skills/base44-platform-games/references/phaser/text_and_bitmaptext.md": "99fc0b2503bc6a6ed55193ee9a579143a352fc2b32be9556eef6a54a4974b206", - "skills/base44-platform-games/references/phaser/tilemaps.md": "ef5f623a332585fe19568b342fbf02e2e5722d1042705945add1dc5b933c447e", - "skills/base44-platform-games/references/phaser/time_and_timers.md": "ebcce54b7de4586a30d4055ed67ccbf5cffc28061db9afc32c5c8c4c18c315a7", - "skills/base44-platform-games/references/phaser/tweens.md": "c5a7062d488039081ed7b24013fa13e78dea24125c7b972cfcb296436db6d0c5", - "skills/base44-platform-games/references/threejs/assets_models_textures.md": "e011402834044d6e9205df7656726baceaa6aa98d9ade20f586d5ddce9236f29", - "skills/base44-platform-games/references/threejs/cameras.md": "31b598dd5c6b8fa6e40298a26e72fe5260fd1d9020dd768314a12369f8955bea", - "skills/base44-platform-games/references/threejs/character_controller.md": "12f150f38fc51865846b455f43d18b88aeb525e61f26e80f74536797ff7a3bd8", - "skills/base44-platform-games/references/threejs/enemies_and_movement.md": "e43c30fb7bed6d33749d483a1aa563ec3b205dcf76ad22d083c9f4c6eaa81dac", - "skills/base44-platform-games/references/threejs/game_state_and_flow.md": "9f4224f5453e30df5a206010fb8db8c3a02eadbeb6b4de569901be17bc566ef5", - "skills/base44-platform-games/references/threejs/input_and_controls.md": "56bd480ea000ad359e644d3b1811bbefe16a0843a4ae4d947ee06fc1c9c6e494", - "skills/base44-platform-games/references/threejs/performance.md": "ed57ede1db9439ee663e71c6d4b2e579c8956c960fe69af79c5e8e0f57a11e4d", - "skills/base44-platform-games/references/threejs/physics_dynamics.md": "75f0017a31867a4fb6a975393c099143ff57e1e031ad277af04c88662f73241c", - "skills/base44-platform-games/references/threejs/projectiles_and_pooling.md": "b2137f885ee8fd63868dae1584676a2e34c0365e1bcc31b60b1ed0b8845352dd", - "skills/base44-platform-games/references/threejs/scene_and_rendering.md": "667ea68cda6b354ffb6d54a7ce5050a661e8a997d3255f1c664440b00bd0cc30", - "skills/base44-platform-games/references/threejs/text_hud_ui.md": "c9bef2170523bf1c679f1608730004fe9c6e9b02254cc1be2102113983d09658", - "skills/base44-platform-games/references/threejs/triggers_and_collisions.md": "f981ba3c28e7f65f11e9f01e082ba24490ddb7e0c5aa9ee961d008a421f47758", - "skills/base44-platform-games/references/threejs/vfx_and_juice.md": "84e3e98d8b72fe01b73b9c1da4b3c77a38f85a0500a2ceb54f18b5fc4cf73b4c", - "skills/base44-platform-integrations-sdk/SKILL.md": "eea0a0061264ed71408db5b38787556d892472a13f2995d8c70464942bc143c2", + "skills/base44-platform-integrations-sdk/SKILL.md": "cc18027267ab6f7c64d34e8d26d3aa4b3fcfe2d6c35b2c81468a2351c6658997", "skills/base44-platform-integrations-sdk/references/mobile.md": "234428eb01d45bd67569840dd32404018f1b60efacddd98908798cc57e7cc843", "skills/base44-platform-integrations-sdk/references/web-spa.md": "6f384fbd1ce1eb822fddd55503053af25d1ecce709ed3988ec45efbabba0d232", "skills/base44-platform-integrations-sdk/references/web-tanstack.md": "c7baee1ab96aac7196753baa8106d597711662b6c87149a60412667b34b197e0", - "skills/base44-platform-platform/SKILL.md": "a3d6c63e17a6227315c8351cd7756db6612ad5e7a26aee844773073ec68ffcaf", - "skills/base44-platform-platform/references/game.md": "5693f5d4c492fa669d951e8f2e85e713cf100009f8db9a5a0aa1842a74e5efba", + "skills/base44-platform-platform/SKILL.md": "cdcec44e4885c6970cc6f79880194fbe7dd99c2aedad9480d4afca18ce339a19", "skills/base44-platform-platform/references/mobile.md": "bc5116e7e7896f56908dd8e400cb7b7101f0ee53597725a0af614002b1ac669a", "skills/base44-platform-platform/references/web-spa.md": "52917f4b90d778d21e73b70ab7862e9250044831b51db4dd8723cf68a6706941", "skills/base44-platform-platform/references/web-tanstack.md": "625c2c6f75647c843292b9a864db8b737b674848cae1d9e44e9630185c8429d9", - "skills/base44-platform-workflows/SKILL.md": "59a92e17dd5d7c4a151a9733f1218749cce571b7101315a04b307446b74d0726", + "skills/base44-platform-workflows/SKILL.md": "fbe451b408f73831a830094c3968ae15ef27f846138808c54039ef24f7dc56c4", "skills/base44-platform-workflows/references/automations-legacy.md": "0518e4aaab82d2e2ff842fce5eb7daeb140900eb48a952a3528e7cc84e2db502", "skills/base44-platform-workflows/references/backend-functions.md": "92307752be105d6db7e13576740785281e1812dec508f66a2b530312973b71f3", "skills/base44-platform-workflows/references/mobile-automations.md": "976a3adca97c0023352bccdefa36a6330a90e4db48d67aa3694f0dedf646de67", diff --git a/skills/base44-platform-platform/SKILL.md b/skills/base44-platform-platform/SKILL.md index b758144..87f21d4 100644 --- a/skills/base44-platform-platform/SKILL.md +++ b/skills/base44-platform-platform/SKILL.md @@ -15,12 +15,10 @@ Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (ov tool, or with your Base44 CLI token); it returns the framework, app type, runtime, entitlements and the exact reference files to read. Without it, detect from the project files: `src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a -native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. +native mobile (Expo) app; otherwise it is a Vite SPA. | Profile | Read | |---|---| | Web app, Vite SPA (the default web template) | [Web app, Vite SPA](references/web-spa.md) | | Web app, TanStack Start | [Web app, TanStack Start](references/web-tanstack.md) | | Native mobile app (Expo, React Native) | [Native mobile app (Expo)](references/mobile.md) | -| Game, 2D (Phaser 4) | [Game (Phaser 2D or Three.js 3D)](references/game.md) | -| Game, 3D (Three.js) | [Game (Phaser 2D or Three.js 3D)](references/game.md) | diff --git a/skills/base44-platform-platform/references/game.md b/skills/base44-platform-platform/references/game.md deleted file mode 100644 index f6fd9c5..0000000 --- a/skills/base44-platform-platform/references/game.md +++ /dev/null @@ -1,20 +0,0 @@ -<!-- GENERATED FILE. DO NOT EDIT. Compiled from the Base44 builder's prompt sources; the - source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by - the next sync; change the source instead. --> -# Base44 platform: Game (Phaser 2D or Three.js 3D) - -<technology_stack> -Base44 games are built on React + Tailwind CSS, inside Base44's React shell. 2D games run on Phaser 4; 3D games run on Three.js through React Three Fiber (no Phaser). Other frameworks (Next.js, Python, etc.) are not supported. Do NOT combine Phaser and Three.js in one game unless the user explicitly asks for it. -Games are using Base44's backend as a service - that gives them data management, integrations, and more. - -2D vs 3D is settled when the game is created; the engine in use is in the app profile (`game_engine`). Converting an existing game between 2D and 3D is a builder operation: ask the user to switch the engine in the Base44 builder, then rebuild in the new engine. - -The React overlay always renders ABOVE the game canvas — Phaser or Three.js. A HUD button that is visible but not clickable is missing `pointer-events-auto` (or the canvas is stacked above it). -</technology_stack> - -<backend> -- entities: entities are defined as json schema objects that define the shape of the data that is stored in the app. -- functions: backend functions, that are use mainly to integrate with external APIs, and are defined in the `functions` folder, as HTTP handlers. -- automations: automations run backend functions automatically. Three types: scheduled (cron/interval), entity (on record create/update/delete), and connector (on external webhook events from connected services like Google Calendar, Gmail, Slack, etc.). -- `base44/` is server-side and never in the client bundle: never import it from a frontend file (the game calls `base44.functions.invoke(...)` instead), and never import `src/` or `@/` from a function. -</backend> diff --git a/skills/base44-platform-workflows/SKILL.md b/skills/base44-platform-workflows/SKILL.md index 605ecd9..1761357 100644 --- a/skills/base44-platform-workflows/SKILL.md +++ b/skills/base44-platform-workflows/SKILL.md @@ -17,7 +17,7 @@ Determine the app profile first. Call `GET /api/apps/{app_id}/agent-profile` (ov tool, or with your Base44 CLI token); it returns the framework, app type, runtime, entitlements and the exact reference files to read. Without it, detect from the project files: `src/routes/__root.*` and `src/routeTree.gen.ts` mean TanStack Start; `app/_layout.*` means a -native mobile (Expo) app; `src/game/2d/` or `src/game/3d/` means a game; otherwise it is a Vite SPA. +native mobile (Expo) app; otherwise it is a Vite SPA. | Profile | Read | |---|---| From 3d057a3bead46c1f7afc916cb8acdf8f5fb79a53 Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Sun, 1 Nov 2026 11:28:43 +0000 Subject: [PATCH 14/15] chore(base44-platform): recompile from apper 025f298e0d9; human-merge label skips auto-merge Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2fHMwsHuKC2HxRkojwH2X --- .github/workflows/auto-merge-skill-update.yml | 4 +- SOURCE_COMMIT | 2 +- .../references/web-spa.md | 2 +- .../references/web-tanstack.md | 2 +- .../references/mobile.md | 17 +- .../references/web-spa.md | 19 +- .../references/web-tanstack.md | 7 + .../references/mobile.md | 175 +++++++++++++++++- .../references/web-spa.md | 171 ++++++++++++++++- .../references/web-tanstack.md | 171 ++++++++++++++++- .../references/web-spa.md | 1 + .../references/web-tanstack.md | 2 +- .../references/mobile.md | 2 +- .../references/web-spa.md | 2 +- .../references/web-tanstack.md | 2 +- skills/base44-platform-manifest.json | 30 +-- .../references/automations-legacy.md | 20 +- .../references/mobile-automations.md | 48 +---- 18 files changed, 586 insertions(+), 91 deletions(-) diff --git a/.github/workflows/auto-merge-skill-update.yml b/.github/workflows/auto-merge-skill-update.yml index 8baa5f3..9a30d75 100644 --- a/.github/workflows/auto-merge-skill-update.yml +++ b/.github/workflows/auto-merge-skill-update.yml @@ -5,6 +5,8 @@ name: Auto-merge compiled skill updates # touches only the compiled `skills/base44-platform-*` tree and the SOURCE_COMMIT pin is merged # automatically once `Validate base44-platform skills` passes. Anything else with the label (the # CLI / SDK sync PRs, a hand-made PR) is left for a human. +# A sync PR labelled `human-merge` was published on a person's approval after an IP review +# finding: it is left for a human too, even when a later push updates it. # # Requires: GitHub auto-merge enabled on this repository and the validator as a required check # on main. See CONTRIBUTING.md, "Compiled platform skills". @@ -19,7 +21,7 @@ permissions: jobs: enable-auto-merge: - if: contains(github.event.pull_request.labels.*.name, 'auto-skill-update') && github.event.pull_request.user.login == 'base44-github-actions[bot]' + if: contains(github.event.pull_request.labels.*.name, 'auto-skill-update') && !contains(github.event.pull_request.labels.*.name, 'human-merge') && github.event.pull_request.user.login == 'base44-github-actions[bot]' runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/SOURCE_COMMIT b/SOURCE_COMMIT index 6664880..6403575 100644 --- a/SOURCE_COMMIT +++ b/SOURCE_COMMIT @@ -1 +1 @@ -35b6b8c21d65725471706a50c5533abb2ea319e4 +025f298e0d9c239575381d939a226ed1cf47bfea diff --git a/skills/base44-platform-agents/references/web-spa.md b/skills/base44-platform-agents/references/web-spa.md index 44715a4..b973494 100644 --- a/skills/base44-platform-agents/references/web-spa.md +++ b/skills/base44-platform-agents/references/web-spa.md @@ -21,7 +21,7 @@ An agent is a JSON configuration file in the base44/agents/ directory; to delete <tools> Agents act as the current app user, but get no app data/function tools automatically. -New agents start with `"tool_configs": []`. Add only the entity/backend-function permissions, plus app-user connector permissions, the agent actually needs, and tell the user which permissions you granted and why. When later adding a backend function, connector-link capability, or entity capability to an existing agent, change that agent's `tool_configs` only — leave unrelated agents untouched. +New agents start with `"tool_configs": []`. Add only the entity/backend-function permissions the agent actually needs to `tool_configs`, and app-user connector permissions to `app_user_connector_configs` (`{"connector_id": "<connector id>", "enabled": true}`); tell the user which permissions you granted and why. When later adding a backend function, connector-link capability, or entity capability to an existing agent, edit that agent's config file only — leave unrelated agents untouched. App-user OAuth connector permission only exposes connector context and the connect-link tool — it does not grant API access and is not required for backend function tools. Request it when the agent should help app users connect or reconnect that account. diff --git a/skills/base44-platform-agents/references/web-tanstack.md b/skills/base44-platform-agents/references/web-tanstack.md index e137a1a..bec8140 100644 --- a/skills/base44-platform-agents/references/web-tanstack.md +++ b/skills/base44-platform-agents/references/web-tanstack.md @@ -21,7 +21,7 @@ An agent is a JSON configuration file in the base44/agents/ directory; to delete <tools> Agents act as the current app user, but get no app data/server-route tools automatically. -New agents start with `"tool_configs": []`. Add only the entity/server-route permissions, plus app-user connector permissions, the agent actually needs, and tell the user which permissions you granted and why. When later adding a server route, connector-link capability, or entity capability to an existing agent, change that agent's `tool_configs` only — leave unrelated agents untouched. +New agents start with `"tool_configs": []`. Add only the entity/server-route permissions the agent actually needs to `tool_configs`, and app-user connector permissions to `app_user_connector_configs` (`{"connector_id": "<connector id>", "enabled": true}`); tell the user which permissions you granted and why. When later adding a server route, connector-link capability, or entity capability to an existing agent, edit that agent's config file only — leave unrelated agents untouched. App-user OAuth connector permission only exposes connector context and the connect-link tool — it does not grant API access and is not required for server route tools. Request it when the agent should help app users connect or reconnect that account. diff --git a/skills/base44-platform-backend-functions/references/mobile.md b/skills/base44-platform-backend-functions/references/mobile.md index 7223d52..0ea7414 100644 --- a/skills/base44-platform-backend-functions/references/mobile.md +++ b/skills/base44-platform-backend-functions/references/mobile.md @@ -11,6 +11,13 @@ The Core-integration routing rule is an exception: use a backend function for Co For external webhooks, browser GET links and OAuth callbacks, use the server-set `X-Base44-App-Url` origin plus `/functions/<functionName>`. Never expose deployment URLs. Preserve callback query parameters and validate OAuth state in the handler. +<guest_checkout> +- Narrow exception to the authentication/service-role rules: public checkout initiation may run without login, including a server-side payment-provider call and creation of a new pending order. Do not report missing authentication solely for that flow when the server selects the payable products, prices and currency from server configuration, provider-owned prices, or catalog records buyers cannot create, update or delete, validates quantities, returns only the new checkout's details, and neither exposes nor changes existing customer/order/entitlement records chosen by the caller. Reading the public catalog through service role does not invalidate this exception. This does not exempt other anonymous writes, messages, paid AI calls or privileged payment operations. +- Report concrete payment trust-boundary failures: caller-controlled amounts/prices/currency or payment status, access to another customer's records, or fulfillment/pass/access granted before server-side payment-provider verification. A success redirect, a client-supplied session/order id, or the presence of a signature header alone is not proof of payment. +- When fixing payment findings, preserve intended guest checkout and its successful response shape. Fix pricing, record scoping and payment verification at the relevant boundary; do not require login or a browser-embedded shared secret merely to start checkout. +- Origin/Referer/Host headers are not caller authentication or proof of payment. Do not add an Origin-equals-Host restriction as a security fix: legitimate custom-domain, platform-domain and preview callers may use different API hosts. Preserve platform app/Host binding and legitimate redirect/CORS allowlists; check both custom-domain and platform-domain guest checkout after changes. +</guest_checkout> + <code_validation> **Validation Flow**: After you generate the function code, it automatically goes through lint validation before deployment. @@ -28,7 +35,7 @@ For external webhooks, browser GET links and OAuth callbacks, use the server-set - must not redeclare variables already in scope (e.g. `user`, `req`) - must prefix **external-dependency** imports with `npm:package@version` (relative imports of your own files are not external deps) - must return Response objects - never return strings or plain objects directly -- must include proper try/catch error handling for robustness +- must return 4xx for caller errors before the work, never `throw` them into the catch: 400 bad/missing input (an unparseable body too: `await req.json().catch(() => ({}))`), 401/403 auth, 404 missing record; SDK calls reject instead of returning null (`auth.me()` signed out: 401, `entities.X.get(id)` on a missing id: 404): before the `if (!x)` guard, turn only that status into null (`.catch((e) => (e.status === 404 ? null : Promise.reject(e)))`), or the guard never runs and the catch returns 500; a 429 or outage still rejects; try/catch the rest — the catch's 500 means the function crashed - must validate user authentication with `base44.auth.me()` when using Base44 SDK - Exception: use `base44.asServiceRole.integrations.Core` for Core-integration functions. Keep the function narrowly scoped: validate and bound its input, return only the needed result, and do not use the service role for entities or other app data. - must use proper JavaScript/TypeScript syntax with matched brackets and braces @@ -120,7 +127,7 @@ import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54'; export default async function(req) { try { const base44 = createClientFromRequest(req); - const user = await base44.auth.me(); + const user = await base44.auth.me().catch((e) => (e.status === 401 ? null : Promise.reject(e))); if (!user) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); @@ -176,7 +183,7 @@ When creating backend functions that should ONLY be invoked by admin (e.g., sche <example> const base44 = createClientFromRequest(req); -const user = await base44.auth.me(); +const user = await base44.auth.me().catch((e) => (e.status === 401 ? null : Promise.reject(e))); if (user?.role !== 'admin') { return Response.json({ error: 'Forbidden: Admin access required' }, { status: 403 }); } @@ -225,7 +232,7 @@ export default async function(req) { try { const base44 = createClientFromRequest(req); - const user = await base44.auth.me(); + const user = await base44.auth.me().catch((e) => (e.status === 401 ? null : Promise.reject(e))); if (!user) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); } @@ -275,7 +282,7 @@ import { jsPDF } from 'npm:jspdf@4.0.0'; export default async function(req) { try { const base44 = createClientFromRequest(req); - const user = await base44.auth.me(); + const user = await base44.auth.me().catch((e) => (e.status === 401 ? null : Promise.reject(e))); if (!user) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); diff --git a/skills/base44-platform-backend-functions/references/web-spa.md b/skills/base44-platform-backend-functions/references/web-spa.md index 9851aa4..ac3a56c 100644 --- a/skills/base44-platform-backend-functions/references/web-spa.md +++ b/skills/base44-platform-backend-functions/references/web-spa.md @@ -17,6 +17,13 @@ For external-service integrations, in order: </integration_priority> +<guest_checkout> +- Narrow exception to the authentication/service-role rules: public checkout initiation may run without login, including a server-side payment-provider call and creation of a new pending order. Do not report missing authentication solely for that flow when the server selects the payable products, prices and currency from server configuration, provider-owned prices, or catalog records buyers cannot create, update or delete, validates quantities, returns only the new checkout's details, and neither exposes nor changes existing customer/order/entitlement records chosen by the caller. Reading the public catalog through service role does not invalidate this exception. This does not exempt other anonymous writes, messages, paid AI calls or privileged payment operations. +- Report concrete payment trust-boundary failures: caller-controlled amounts/prices/currency or payment status, access to another customer's records, or fulfillment/pass/access granted before server-side payment-provider verification. A success redirect, a client-supplied session/order id, or the presence of a signature header alone is not proof of payment. +- When fixing payment findings, preserve intended guest checkout and its successful response shape. Fix pricing, record scoping and payment verification at the relevant boundary; do not require login or a browser-embedded shared secret merely to start checkout. +- Origin/Referer/Host headers are not caller authentication or proof of payment. Do not add an Origin-equals-Host restriction as a security fix: legitimate custom-domain, platform-domain and preview callers may use different API hosts. Preserve platform app/Host binding and legitimate redirect/CORS allowlists; check both custom-domain and platform-domain guest checkout after changes. +</guest_checkout> + <secrets> Before writing a function that needs a secret/API key, check it's set — using an unset secret fails. If missing, ask the user to add it on the app's Dashboard → Secrets page (shown once backend functions are on) and wait for their confirmation before writing code that reads it. Secrets: `import { secrets } from "base44:runtime"` → `secrets.get("NAME")`; pre-populated: BASE44_APP_ID. Users manage secrets on the Dashboard "Secrets" page (its own sidebar item, shown once backend functions are on), not in Settings. @@ -32,7 +39,7 @@ Requirements: - Handler: `export default async function(req: Request): Promise<Response> { ... }` — the only accepted pattern. - Return Response objects, not strings/plain objects. - All initialization inside the handler — module-top-level `throw`/`await` = boot error instead of a 500: no try/catch reached, no app logs. -- try/catch the handler body; return a proper error response. +- Caller errors return 4xx before the work, never `throw` into the catch: 400 bad/missing input (an unparseable body too: `await req.json().catch(() => ({}))`), 401/403 auth, 404 missing record. SDK calls reject instead of returning null (`auth.me()` signed out: 401, `entities.X.get(id)` on a missing id: 404): before the `if (!x)` guard, turn only that status into null (`.catch((e) => (e.status === 404 ? null : Promise.reject(e)))`), or the guard never runs and the catch returns 500; a 429 or outage still rejects. try/catch the rest; the catch's 500 means the function crashed. - `await` every Base44 SDK call — `base44.entities.X.list()`, `base44.auth.me()`, `base44.functions.invoke(...)` all return Promises. - Validate user auth with `base44.auth.me()` when using the SDK. - Exception: use `base44.asServiceRole.integrations.Core` for Core-integration functions. Keep the function narrowly scoped: validate and bound its input, return only the needed result, and do not use the service role for entities or other app data. @@ -70,12 +77,16 @@ import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54'; export default async function(req) { try { const base44 = createClientFromRequest(req); - const user = await base44.auth.me(); + const user = await base44.auth.me().catch((e) => (e.status === 401 ? null : Promise.reject(e))); if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 }); + const { id } = await req.json().catch(() => ({})); + if (!id) return Response.json({ error: 'id is required' }, { status: 400 }); + const record = await base44.entities.SampleData.get(id).catch((e) => (e.status === 404 ? null : Promise.reject(e))); + if (!record) return Response.json({ error: 'Not found' }, { status: 404 }); const { items } = await base44.entities.SampleData.filter({ status: 'open' }, { sort: '-created_date', limit: 50 }); // user-scoped (app-user token), one page const total = await base44.asServiceRole.entities.SampleData.count(); // service-scoped (admin), a number: no records read const res = await base44.functions.invoke('myFunction', { foo: 'bar' }); // another backend function - return Response.json({ items, total }); + return Response.json({ record, items, total }); } catch (error) { return Response.json({ error: error.message }, { status: 500 }); } @@ -136,7 +147,7 @@ import { secrets } from 'base44:runtime'; export default async function(req) { try { const base44 = createClientFromRequest(req); - const user = await base44.auth.me(); + const user = await base44.auth.me().catch((e) => (e.status === 401 ? null : Promise.reject(e))); if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 }); // SDK clients constructed inside the handler — module-level init throws on missing secrets and crashes boot. const openai = new OpenAI({ apiKey: secrets.get("OPENAI_API_KEY") }); diff --git a/skills/base44-platform-backend-functions/references/web-tanstack.md b/skills/base44-platform-backend-functions/references/web-tanstack.md index e572c0f..5650bb4 100644 --- a/skills/base44-platform-backend-functions/references/web-tanstack.md +++ b/skills/base44-platform-backend-functions/references/web-tanstack.md @@ -23,6 +23,13 @@ A failed SDK call rejects with an error carrying the HTTP `status`; `entities.<N Server functions and server routes are public endpoints: anyone can call them, with any input. Before `asServiceRole` or credit-spending work (Core integrations such as InvokeLLM), verify the caller and that the business logic allows it: `.middleware([requireUser])` from `@/lib/auth-middleware` (`createServerFn({ method: "POST" }).middleware([requireUser]).handler(({ context }) => …)`) rejects anonymous callers with 401 and sets `context.user`; a server route whose callers send a Base44 token uses `server: { middleware: [requireUserRoute], handlers }` from the same module, the same way (only on `src/routes/api/*` files — a route's middleware also runs for every route under it). Links and images the browser loads by itself carry no token, so a route serving them is public or checks a short-lived signed token a server function issued. Admin-only handlers also require `context.user.role === "admin"` and reject anyone else with 403. Skip the sign-in check only for a deliberately public feature (a no-login app whose visitors are meant to trigger it), and bound its input: `.max()` on every string and array, `z.enum` for choices. Webhooks verify the request's signature or shared secret instead. </caller_auth> +<guest_checkout> +- Narrow exception to the authentication/service-role rules: public checkout initiation may run without login, including a server-side payment-provider call and creation of a new pending order. Do not report missing authentication solely for that flow when the server selects the payable products, prices and currency from server configuration, provider-owned prices, or catalog records buyers cannot create, update or delete, validates quantities, returns only the new checkout's details, and neither exposes nor changes existing customer/order/entitlement records chosen by the caller. Reading the public catalog through service role does not invalidate this exception. This does not exempt other anonymous writes, messages, paid AI calls or privileged payment operations. +- Report concrete payment trust-boundary failures: caller-controlled amounts/prices/currency or payment status, access to another customer's records, or fulfillment/pass/access granted before server-side payment-provider verification. A success redirect, a client-supplied session/order id, or the presence of a signature header alone is not proof of payment. +- When fixing payment findings, preserve intended guest checkout and its successful response shape. Fix pricing, record scoping and payment verification at the relevant boundary; do not require login or a browser-embedded shared secret merely to start checkout. +- Origin/Referer/Host headers are not caller authentication or proof of payment. Do not add an Origin-equals-Host restriction as a security fix: legitimate custom-domain, platform-domain and preview callers may use different API hosts. Preserve platform app/Host binding and legitimate redirect/CORS allowlists; check both custom-domain and platform-domain guest checkout after changes. +</guest_checkout> + <secrets> Before writing server code that needs an API key or secret, check it is set — an unset secret fails. If missing, ask the user to add it on the app's Dashboard → Secrets page and wait for their confirmation before writing code that reads it. Read it with `import { secrets } from "base44:runtime"` → `secrets.get("NAME")` (`undefined` if unset) only in server functions, server routes and `*.server.js` files; never `process.env` or a `VITE_*` var (build-time, client-visible). A set or rotated secret reaches the dev preview and the latest commit's worker within seconds — no redeploy; a published app behind the latest commit picks it up on its next publish. </secrets> diff --git a/skills/base44-platform-connectors/references/mobile.md b/skills/base44-platform-connectors/references/mobile.md index 1bf6d42..dd349d0 100644 --- a/skills/base44-platform-connectors/references/mobile.md +++ b/skills/base44-platform-connectors/references/mobile.md @@ -8,9 +8,9 @@ App connectors allow you to integrate with external services in an easy way usin App connectors come in the following types: <connector_types> -1. **Shared connector**: The app builder connects their own account. All app users share the app builder's single connection. Backend functions call the external API on the app builder's behalf. See the connector's details (`list_connectors`). -2. **App user connector**: The app builder provides their OAuth app credentials (client ID, client secret). Each App User connects their own account individually. Backend functions call the external API on the app user's behalf using the app user's tokens. See the connector's details (`list_connectors`). -3. **BYO shared connector**: A workspace admin registers their own OAuth app (an `OrganizationConnector`). The app builder consents once against it; the resulting token is shared across all app users. Use when the platform doesn't have its own OAuth app for the integration (e.g., Snowflake, Databricks) or when the workspace wants to use its own OAuth app. See the connector's details (`list_connectors`). +1. **Shared connector**: The app builder connects their own account. All app users share the app builder's single connection. Backend functions call the external API on the app builder's behalf. See <connector_workflow_rules>. +2. **App user connector**: The app builder provides their OAuth app credentials (client ID, client secret). Each App User connects their own account individually. Backend functions call the external API on the app user's behalf using the app user's tokens. See <app_user_connectors>. +3. **BYO shared connector**: A workspace admin registers their own OAuth app (an `OrganizationConnector`). The app builder consents once against it; the resulting token is shared across all app users. Use when the platform doesn't have its own OAuth app for the integration (e.g., Snowflake, Databricks) or when the workspace wants to use its own OAuth app. See <byo_shared_connectors>. </connector_types> Backend functions MUST be enabled for each connector type. @@ -32,6 +32,173 @@ App connectors require a Builder+ subscription; the app profile reports the work When the user asks to connect or use an external service: 1. If the catalog has no connector for the service, do not use the connector APIs. Integrate with backend functions with secrets or direct API calls instead. 2. Read the connector's details (description, scopes, auth status, connection mode, usage guide) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available`, and follow its connection mode: shared (one account serves every app user) or app user (each user connects their own). -3. Write connector-backed code only once the connector is authorized; in server code read its token with `base44.asServiceRole.connectors.getConnection(integration_type)` and make the API calls yourself. +3. Write connector-backed code only once the connector is authorized, and read its token in server code by connection mode: shared → `base44.asServiceRole.connectors.getConnection(integration_type)`; BYO shared → `base44.asServiceRole.connectors.getWorkspaceConnection(connectorId)` (see <byo_shared_connectors>); app user → `base44.asServiceRole.connectors.getCurrentAppUserConnection(connectorId)` for the signed-in app user, after the frontend let them consent with `connectAppUser(connectorId)` (see <app_user_connectors>). Never serve one app user's data through the shared account's token. Then make the API calls yourself. </connector_workflow_rules> </app_connectors> + +<byo_shared_connectors> +A workspace-registered OAuth app (an `OrganizationConnector`) that the app builder consents once against. The resulting token is shared across all app users — same semantics as a SHARED connector, but the OAuth credentials come from the workspace admin (not the platform) and so the connection is keyed by `connectorId` rather than `integration_type`. + +**Prerequisite — the workspace connector must already exist.** A workspace admin registers it in workspace settings → Connectors ("Manage connectors" for users on the new integrations view) with the OAuth client_id, client_secret and any integration-specific config such as a Snowflake subdomain; its `id` is the `connectorId` below. If none exists for the integration the user needs, tell the user to have their admin register one, then retry. + +The app builder consents once against the connector from the Base44 builder chat (ask it to connect the workspace connector); the MCP `initiate_connector_connection` tool connects platform-shared connectors by integration type only. + +**No frontend code is generated for this mode** — consent is a one-time build-time action, not a runtime action in the published app. Unlike APP_USER, there is no `connectAppUser` call. + +**Backend function — use `getWorkspaceConnection(connectorId)`:** + +``` +import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54'; + +export default async function(req) { + try { + const base44 = createClientFromRequest(req); + + // connectorId is the workspace connector's id, e.g. "abc123" + const { accessToken, connectionConfig } = await base44.asServiceRole.connectors.getWorkspaceConnection("abc123"); + + // connectionConfig carries any admin-configured parameters such as + // subdomain, workspace URL, etc. Consult the integration's usage guide + // for the exact field name. + const apiUrl = `https://${connectionConfig.subdomain}.snowflakecomputing.com/api/v2/statements`; + + const response = await fetch(apiUrl, { + method: "POST", + headers: { + "Authorization": `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ statement: "SELECT COUNT(*) FROM my_table" }), + }); + const data = await response.json(); + return Response.json(data); + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }); + } +} +``` + +Differences from SHARED code-gen: +- A dedicated `getWorkspaceConnection(connectorId)` method is called with the connector's `id` as a string — NOT `getConnection`, which is reserved for platform-shared connectors keyed by `"<integration_type>"`. +- `connectionConfig` is sourced from the workspace admin's `OrganizationConnector`, not per-app — so whatever the admin configured at registration time is what you get at runtime. +</byo_shared_connectors> + +<app_user_connectors> +App-user connectors are registered per workspace (workspace settings → Connectors); each has an `id`, the `connectorId` below. If one is already registered for that integration type, use its `id` — do not register another. Register a second connector of the same type only when the user asks for a separate one (a different account or OAuth app); when that is unclear, ask which they want instead of registering. + +When none is registered yet, the user registers one there with the integration type, a name no existing connector uses, a description and the minimal scopes the request needs — suggest those from the provider's OAuth documentation. + +When implementing app-user connector functionality: +1. **Connector**: Use the connector already registered for that integration type; the user registers a new one only when none exists. +2. **Backend function**: Reuse an existing backend function when it already provides the requested connector-backed behavior. If no suitable function exists, create or edit one that uses `base44.asServiceRole.connectors.getCurrentAppUserConnection(connectorId)` to get the *current app user's* connection (access token + connection config) and call the external API. +3. **Frontend**: Generate the connect/disconnect UI and data fetching, following the three rules in `<app_user_connector_frontend_rules>`. Use the connector's `id` as the `connectorId`. +4. **In-app agents**: If a specific agent should use this functionality, add the backend function to its `tool_configs`. Give it app-user connector permission only when it should help app users connect or reconnect that account: in the agent's config, `app_user_connector_configs` takes the exact connector `id` as `connector_id`; do not pass the integration type or connector name. + +<app_user_connector_sdk> +**APP_USER mode** — requires BOTH frontend code (to let each app user consent) AND a backend function (to use the current app user's token). + +All APP_USER SDK methods are keyed on **connectorId** (the connector's `id` in the dashboard) — NOT the integration type. + +**SDK methods:** + +| Connector mode | Frontend | Backend function | +|---|---|---| +| SHARED | None (no frontend SDK) | `base44.asServiceRole.connectors.getConnection(type)` | +| BYO_SHARED | None (no frontend SDK — consent is at build time) | `base44.asServiceRole.connectors.getWorkspaceConnection(connectorId)` | +| APP_USER | `base44.connectors.connectAppUser(connectorId)`, `base44.connectors.disconnectAppUser(connectorId)` | `base44.asServiceRole.connectors.getCurrentAppUserConnection(connectorId)` | + +- `connectAppUser(connectorId)` — returns a URL string. Open it so the app user can complete OAuth consent. +- `disconnectAppUser(connectorId)` — revokes the OAuth connection server-side, returns nothing. +- `getCurrentAppUserConnection(connectorId)` — returns a connection object `{ accessToken, connectionConfig }` for the **app user's** OAuth connection (not the builder's). Destructure `accessToken` and optionally `connectionConfig` (e.g. `const { accessToken, connectionConfig } = await ...getCurrentAppUserConnection(connectorId)`). `connectionConfig` may contain connector-specific parameters (e.g. subdomain, workspace URL) — only use it when the connector's usage guide says to. + +**Backend function example:** + +``` +import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54'; + +export default async function(req) { + try { + const base44 = createClientFromRequest(req); + const user = await base44.auth.me(); + if (!user) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // connectorId is the registered connector's id, e.g. "abc123" + const { accessToken, connectionConfig } = await base44.asServiceRole.connectors.getCurrentAppUserConnection(connectorId); + + const response = await fetch('https://www.googleapis.com/calendar/v3/calendars/primary/events', { + headers: { 'Authorization': `Bearer ${accessToken}` } + }); + const data = await response.json(); + return Response.json({ events: data.items || [] }); + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }); + } +} +``` +</app_user_connector_sdk> + +<app_user_connector_frontend_rules> +**Three rules you MUST follow when generating frontend code that uses app user connectors. Design the page UI however fits the app — but always apply these rules:** + +**Rule 1 — Auth gate:** `connectAppUser()` will fail if the app user is not signed in. Use `base44.auth.isAuthenticated()` to check before rendering any connector UI (Connect button, data that depends on the connector). If not authenticated, show a login button that calls `base44.auth.redirectToLogin()`. + +**Rule 2 — Connection status via data fetch:** There is no separate "check connection" API. Instead, call the backend function that uses the connector on page load inside a try/catch. If it succeeds → user is connected (and you already have the data). If it throws → user is not connected, show the Connect button. Extract this fetch as a reusable function — you will call it again in Rule 3. + +**Rule 3 — Auto-refresh after OAuth:** The OAuth flow opens in a new tab. When the user finishes and the tab closes, the original page doesn't know. To fix this: use `window.open()` to keep a reference to the popup, then poll `popup.closed` — when it closes, call the reusable fetch from Rule 2 to refresh data automatically. + +**How the three rules connect (hooks/handlers skeleton):** +``` +const [user, setUser] = useState(null); +const [data, setData] = useState(null); +const [connected, setConnected] = useState(false); +const [loading, setLoading] = useState(true); + +// Rule 2: reusable fetch — doubles as connection check AND data loader +const fetchData = async () => { + try { + const res = await base44.functions.invoke("myBackendFunction", {}); + setData(res.data); + setConnected(true); + } catch { + setConnected(false); + } +}; + +// Rule 1+2: check auth first, then fetch to detect connection status +useEffect(() => { + base44.auth.isAuthenticated().then(async (authed) => { + if (authed) { + const me = await base44.auth.me(); + setUser(me); + await fetchData(); + } + setLoading(false); + }); +}, []); + +// Rule 3: open OAuth popup, poll for close, then re-fetch +const handleConnect = async () => { + const url = await base44.connectors.connectAppUser(CONNECTOR_ID); + const popup = window.open(url, "_blank"); + const timer = setInterval(() => { + if (!popup || popup.closed) { + clearInterval(timer); + fetchData(); + } + }, 500); +}; + +const handleDisconnect = async () => { + await base44.connectors.disconnectAppUser(CONNECTOR_ID); + setConnected(false); + setData(null); +}; + +// Then in your JSX, use loading, user, and connected to decide what to render. +// If !user, show a login button that calls base44.auth.redirectToLogin(). +// Design the UI however fits the app. +``` +</app_user_connector_frontend_rules> +</app_user_connectors> diff --git a/skills/base44-platform-connectors/references/web-spa.md b/skills/base44-platform-connectors/references/web-spa.md index b320f49..4af363f 100644 --- a/skills/base44-platform-connectors/references/web-spa.md +++ b/skills/base44-platform-connectors/references/web-spa.md @@ -7,7 +7,7 @@ App connectors integrate the app with external services via OAuth. <connector_types> -1. Shared: builder connects their own account; all app users share it; backend functions act on the builder's behalf. See <shared_connectors>. +1. Shared: builder connects their own account; all app users share it; backend functions act on the builder's behalf. See <connector_workflow_rules>. 2. App user: builder provides OAuth app credentials (client ID, client secret); each app user connects their own account; backend functions use that user's tokens. See <app_user_connectors>. 3. BYO shared: workspace admin registers their own OAuth app (`OrganizationConnector`); builder consents once; token shared across all app users. For services with no platform OAuth app (e.g., Snowflake, Databricks) or when the workspace wants its own. See <byo_shared_connectors>. </connector_types> @@ -29,6 +29,173 @@ App connectors require a Builder+ subscription; the app profile reports the work When the user asks to connect or use an external service: 1. If the catalog has no connector for the service, do not use the connector APIs. Integrate with backend functions with secrets or direct API calls instead. 2. Read the connector's details (description, scopes, auth status, connection mode, usage guide) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available`, and follow its connection mode: shared (one account serves every app user) or app user (each user connects their own). -3. Write connector-backed code only once the connector is authorized; in server code read its token with `base44.asServiceRole.connectors.getConnection(integration_type)` and make the API calls yourself. +3. Write connector-backed code only once the connector is authorized, and read its token in server code by connection mode: shared → `base44.asServiceRole.connectors.getConnection(integration_type)`; BYO shared → `base44.asServiceRole.connectors.getWorkspaceConnection(connectorId)` (see <byo_shared_connectors>); app user → `base44.asServiceRole.connectors.getCurrentAppUserConnection(connectorId)` for the signed-in app user, after the frontend let them consent with `connectAppUser(connectorId)` (see <app_user_connectors>). Never serve one app user's data through the shared account's token. Then make the API calls yourself. </connector_workflow_rules> </app_connectors> + +<byo_shared_connectors> +A workspace-registered OAuth app (an `OrganizationConnector`) that the app builder consents once against. The resulting token is shared across all app users — same semantics as a SHARED connector, but the OAuth credentials come from the workspace admin (not the platform) and so the connection is keyed by `connectorId` rather than `integration_type`. + +**Prerequisite — the workspace connector must already exist.** A workspace admin registers it in workspace settings → Connectors ("Manage connectors" for users on the new integrations view) with the OAuth client_id, client_secret and any integration-specific config such as a Snowflake subdomain; its `id` is the `connectorId` below. If none exists for the integration the user needs, tell the user to have their admin register one, then retry. + +The app builder consents once against the connector from the Base44 builder chat (ask it to connect the workspace connector); the MCP `initiate_connector_connection` tool connects platform-shared connectors by integration type only. + +**No frontend code is generated for this mode** — consent is a one-time build-time action, not a runtime action in the published app. Unlike APP_USER, there is no `connectAppUser` call. + +**Backend function — use `getWorkspaceConnection(connectorId)`:** + +``` +import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54'; + +export default async function(req) { + try { + const base44 = createClientFromRequest(req); + + // connectorId is the workspace connector's id, e.g. "abc123" + const { accessToken, connectionConfig } = await base44.asServiceRole.connectors.getWorkspaceConnection("abc123"); + + // connectionConfig carries any admin-configured parameters such as + // subdomain, workspace URL, etc. Consult the integration's usage guide + // for the exact field name. + const apiUrl = `https://${connectionConfig.subdomain}.snowflakecomputing.com/api/v2/statements`; + + const response = await fetch(apiUrl, { + method: "POST", + headers: { + "Authorization": `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ statement: "SELECT COUNT(*) FROM my_table" }), + }); + const data = await response.json(); + return Response.json(data); + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }); + } +} +``` + +Differences from SHARED code-gen: +- A dedicated `getWorkspaceConnection(connectorId)` method is called with the connector's `id` as a string — NOT `getConnection`, which is reserved for platform-shared connectors keyed by `"<integration_type>"`. +- `connectionConfig` is sourced from the workspace admin's `OrganizationConnector`, not per-app — so whatever the admin configured at registration time is what you get at runtime. +</byo_shared_connectors> + +<app_user_connectors> +App-user connectors are registered per workspace (workspace settings → Connectors); each has an `id`, the `connectorId` below. If one is already registered for that integration type, use its `id` — do not register another. Register a second connector of the same type only when the user asks for a separate one (a different account or OAuth app); when that is unclear, ask which they want instead of registering. + +When none is registered yet, the user registers one there with the integration type, a name no existing connector uses, a description and the minimal scopes the request needs — suggest those from the provider's OAuth documentation. + +When implementing app-user connector functionality: +1. **Connector**: Use the connector already registered for that integration type; the user registers a new one only when none exists. +2. **Backend function**: Reuse an existing backend function when it already provides the requested connector-backed behavior. If no suitable function exists, create or edit one that uses `base44.asServiceRole.connectors.getCurrentAppUserConnection(connectorId)` to get the *current app user's* connection (access token + connection config) and call the external API. +3. **Frontend**: Generate the connect/disconnect UI and data fetching, following the three rules in `<app_user_connector_frontend_rules>`. Use the connector's `id` as the `connectorId`. +4. **In-app agents**: If a specific agent should use this functionality, add the backend function to its `tool_configs`. Give it app-user connector permission only when it should help app users connect or reconnect that account: in the agent's config, `app_user_connector_configs` takes the exact connector `id` as `connector_id`; do not pass the integration type or connector name. + +<app_user_connector_sdk> +**APP_USER mode** — requires BOTH frontend code (to let each app user consent) AND a backend function (to use the current app user's token). + +All APP_USER SDK methods are keyed on **connectorId** (the connector's `id` in the dashboard) — NOT the integration type. + +**SDK methods:** + +| Connector mode | Frontend | Backend function | +|---|---|---| +| SHARED | None (no frontend SDK) | `base44.asServiceRole.connectors.getConnection(type)` | +| BYO_SHARED | None (no frontend SDK — consent is at build time) | `base44.asServiceRole.connectors.getWorkspaceConnection(connectorId)` | +| APP_USER | `base44.connectors.connectAppUser(connectorId)`, `base44.connectors.disconnectAppUser(connectorId)` | `base44.asServiceRole.connectors.getCurrentAppUserConnection(connectorId)` | + +- `connectAppUser(connectorId)` — returns a URL string. Open it so the app user can complete OAuth consent. +- `disconnectAppUser(connectorId)` — revokes the OAuth connection server-side, returns nothing. +- `getCurrentAppUserConnection(connectorId)` — returns a connection object `{ accessToken, connectionConfig }` for the **app user's** OAuth connection (not the builder's). Destructure `accessToken` and optionally `connectionConfig` (e.g. `const { accessToken, connectionConfig } = await ...getCurrentAppUserConnection(connectorId)`). `connectionConfig` may contain connector-specific parameters (e.g. subdomain, workspace URL) — only use it when the connector's usage guide says to. + +**Backend function example:** + +``` +import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54'; + +export default async function(req) { + try { + const base44 = createClientFromRequest(req); + const user = await base44.auth.me(); + if (!user) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // connectorId is the registered connector's id, e.g. "abc123" + const { accessToken, connectionConfig } = await base44.asServiceRole.connectors.getCurrentAppUserConnection(connectorId); + + const response = await fetch('https://www.googleapis.com/calendar/v3/calendars/primary/events', { + headers: { 'Authorization': `Bearer ${accessToken}` } + }); + const data = await response.json(); + return Response.json({ events: data.items || [] }); + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }); + } +} +``` +</app_user_connector_sdk> + +<app_user_connector_frontend_rules> +**Three rules you MUST follow when generating frontend code that uses app user connectors. Design the page UI however fits the app — but always apply these rules:** + +**Rule 1 — Auth gate:** `connectAppUser()` will fail if the app user is not signed in. Use `base44.auth.isAuthenticated()` to check before rendering any connector UI (Connect button, data that depends on the connector). If not authenticated, show a login button that calls `base44.auth.redirectToLogin()`. + +**Rule 2 — Connection status via data fetch:** There is no separate "check connection" API. Instead, call the backend function that uses the connector on page load inside a try/catch. If it succeeds → user is connected (and you already have the data). If it throws → user is not connected, show the Connect button. Extract this fetch as a reusable function — you will call it again in Rule 3. + +**Rule 3 — Auto-refresh after OAuth:** The OAuth flow opens in a new tab. When the user finishes and the tab closes, the original page doesn't know. To fix this: use `window.open()` to keep a reference to the popup, then poll `popup.closed` — when it closes, call the reusable fetch from Rule 2 to refresh data automatically. + +**How the three rules connect (hooks/handlers skeleton):** +``` +const [user, setUser] = useState(null); +const [data, setData] = useState(null); +const [connected, setConnected] = useState(false); +const [loading, setLoading] = useState(true); + +// Rule 2: reusable fetch — doubles as connection check AND data loader +const fetchData = async () => { + try { + const res = await base44.functions.invoke("myBackendFunction", {}); + setData(res.data); + setConnected(true); + } catch { + setConnected(false); + } +}; + +// Rule 1+2: check auth first, then fetch to detect connection status +useEffect(() => { + base44.auth.isAuthenticated().then(async (authed) => { + if (authed) { + const me = await base44.auth.me(); + setUser(me); + await fetchData(); + } + setLoading(false); + }); +}, []); + +// Rule 3: open OAuth popup, poll for close, then re-fetch +const handleConnect = async () => { + const url = await base44.connectors.connectAppUser(CONNECTOR_ID); + const popup = window.open(url, "_blank"); + const timer = setInterval(() => { + if (!popup || popup.closed) { + clearInterval(timer); + fetchData(); + } + }, 500); +}; + +const handleDisconnect = async () => { + await base44.connectors.disconnectAppUser(CONNECTOR_ID); + setConnected(false); + setData(null); +}; + +// Then in your JSX, use loading, user, and connected to decide what to render. +// If !user, show a login button that calls base44.auth.redirectToLogin(). +// Design the UI however fits the app. +``` +</app_user_connector_frontend_rules> +</app_user_connectors> diff --git a/skills/base44-platform-connectors/references/web-tanstack.md b/skills/base44-platform-connectors/references/web-tanstack.md index e972d00..ad7be74 100644 --- a/skills/base44-platform-connectors/references/web-tanstack.md +++ b/skills/base44-platform-connectors/references/web-tanstack.md @@ -7,7 +7,7 @@ App connectors integrate the app with external services via OAuth. <connector_types> -1. Shared: builder connects their own account; all app users share it; server code acts on the builder's behalf. See <shared_connectors>. +1. Shared: builder connects their own account; all app users share it; server code acts on the builder's behalf. See <connector_workflow_rules>. 2. App user: builder provides OAuth app credentials (client ID, client secret); each app user connects their own account; server code uses that user's tokens. See <app_user_connectors>. 3. BYO shared: workspace admin registers their own OAuth app (`OrganizationConnector`); builder consents once; token shared across all app users. For services with no platform OAuth app (e.g., Snowflake, Databricks) or when the workspace wants its own. See <byo_shared_connectors>. </connector_types> @@ -29,6 +29,173 @@ App connectors require a Builder+ subscription; the app profile reports the work When the user asks to connect or use an external service: 1. If the catalog has no connector for the service, do not use the connector APIs. Integrate with server code with secrets or direct API calls instead. 2. Read the connector's details (description, scopes, auth status, connection mode, usage guide) with the Base44 MCP `list_connectors` tool or `base44 connectors list-available`, and follow its connection mode: shared (one account serves every app user) or app user (each user connects their own). -3. Write connector-backed code only once the connector is authorized; in server code read its token with `base44.asServiceRole.connectors.getConnection(integration_type)` and make the API calls yourself. +3. Write connector-backed code only once the connector is authorized, and read its token in server code by connection mode: shared → `context.getBase44().asServiceRole.connectors.getConnection(integration_type)`; BYO shared → `context.getBase44().asServiceRole.connectors.getWorkspaceConnection(connectorId)` (see <byo_shared_connectors>); app user → `context.getBase44().asServiceRole.connectors.getCurrentAppUserConnection(connectorId)` for the signed-in app user, after the frontend let them consent with `connectAppUser(connectorId)` (see <app_user_connectors>). Never serve one app user's data through the shared account's token. Then make the API calls yourself. </connector_workflow_rules> </app_connectors> + +<byo_shared_connectors> +A workspace-registered OAuth app (an `OrganizationConnector`) that the app builder consents once against. The resulting token is shared across all app users — same semantics as a SHARED connector, but the OAuth credentials come from the workspace admin (not the platform) and so the connection is keyed by `connectorId` rather than `integration_type`. + +**Prerequisite — the workspace connector must already exist.** A workspace admin registers it in workspace settings → Connectors ("Manage connectors" for users on the new integrations view) with the OAuth client_id, client_secret and any integration-specific config such as a Snowflake subdomain; its `id` is the `connectorId` below. If none exists for the integration the user needs, tell the user to have their admin register one, then retry. + +The app builder consents once against the connector from the Base44 builder chat (ask it to connect the workspace connector); the MCP `initiate_connector_connection` tool connects platform-shared connectors by integration type only. + +**No frontend code is generated for this mode** — consent is a one-time build-time action, not a runtime action in the published app. Unlike APP_USER, there is no `connectAppUser` call. + +**Server function — use `getWorkspaceConnection(connectorId)`** (`src/lib/server-fns.js`): + +``` +import { createServerFn } from "@tanstack/react-start"; +import { requireUser } from "@/lib/auth-middleware"; + +// asServiceRole reaches the workspace's shared account: require a signed-in caller, and check +// this user may see what the query returns before returning it. +export const countRows = createServerFn({ method: "GET" }).middleware([requireUser]).handler(async ({ context }) => { + // connectorId is the workspace connector's id, e.g. "abc123" + const { accessToken, connectionConfig } = await context.getBase44().asServiceRole.connectors.getWorkspaceConnection("abc123"); + + // connectionConfig carries any admin-configured parameters such as + // subdomain, workspace URL, etc. Consult the integration's usage guide + // for the exact field name. + const apiUrl = `https://${connectionConfig.subdomain}.snowflakecomputing.com/api/v2/statements`; + + const response = await fetch(apiUrl, { + method: "POST", + headers: { + "Authorization": `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ statement: "SELECT COUNT(*) FROM my_table" }), + }); + if (!response.ok) throw new Error(`Snowflake ${response.status}`); + return await response.json(); // return data, never the token +}); +``` + +Differences from SHARED code-gen: +- A dedicated `getWorkspaceConnection(connectorId)` method is called with the connector's `id` as a string — NOT `getConnection`, which is reserved for platform-shared connectors keyed by `"<integration_type>"`. +- `connectionConfig` is sourced from the workspace admin's `OrganizationConnector`, not per-app — so whatever the admin configured at registration time is what you get at runtime. +</byo_shared_connectors> + +<app_user_connectors> +App-user connectors are registered per workspace (workspace settings → Connectors); each has an `id`, the `connectorId` below. If one is already registered for that integration type, use its `id` — do not register another. Register a second connector of the same type only when the user asks for a separate one (a different account or OAuth app); when that is unclear, ask which they want instead of registering. + +When none is registered yet, the user registers one there with the integration type, a name no existing connector uses, a description and the minimal scopes the request needs — suggest those from the provider's OAuth documentation. + +When implementing app-user connector functionality: +1. **Connector**: Use the connector already registered for that integration type; the user registers a new one only when none exists. +2. **Server function**: Reuse an existing server function when it already provides the requested connector-backed behavior. If none does, add one (`createServerFn` in `src/lib/server-fns.js`) that uses `context.getBase44().asServiceRole.connectors.getCurrentAppUserConnection(connectorId)` to get the *current app user's* connection (access token + connection config) and call the external API. Use a server function, not a server route: the page's call carries the signed-in app user, which is who `getCurrentAppUserConnection` resolves. +3. **Frontend**: Generate the connect/disconnect UI and data fetching, following the three rules in `<app_user_connector_frontend_rules>`. Use the connector's `id` as the `connectorId`. +4. **In-app agents**: Give an agent app-user connector permission only when it should help app users connect or reconnect that account — it gives the agent the connect link, not the provider's API. In the agent's config, `app_user_connector_configs` takes the exact connector `id` as `connector_id`; do not pass the integration type or connector name. + +<app_user_connector_sdk> +**APP_USER mode** — requires BOTH frontend code (to let each app user consent) AND a server function (to use the current app user's token). + +All APP_USER SDK methods are keyed on **connectorId** (the connector's `id` in the dashboard) — NOT the integration type. + +**SDK methods:** + +| Connector mode | Frontend | Server code (`context.getBase44()`) | +|---|---|---| +| SHARED | None (no frontend SDK) | `context.getBase44().asServiceRole.connectors.getConnection(type)` | +| BYO_SHARED | None (no frontend SDK — consent is at build time) | `context.getBase44().asServiceRole.connectors.getWorkspaceConnection(connectorId)` | +| APP_USER | `base44.connectors.connectAppUser(connectorId)`, `base44.connectors.disconnectAppUser(connectorId)` | `context.getBase44().asServiceRole.connectors.getCurrentAppUserConnection(connectorId)` | + +- `connectAppUser(connectorId)` — returns a URL string. Open it so the app user can complete OAuth consent. +- `disconnectAppUser(connectorId)` — revokes the OAuth connection server-side, returns nothing. +- `getCurrentAppUserConnection(connectorId)` — returns a connection object `{ accessToken, connectionConfig }` for the **app user's** OAuth connection (not the builder's). Destructure `accessToken` and optionally `connectionConfig` (e.g. `const { accessToken, connectionConfig } = await ...getCurrentAppUserConnection(connectorId)`). `connectionConfig` may contain connector-specific parameters (e.g. subdomain, workspace URL) — only use it when the connector's usage guide says to. + +**Server function example** (`src/lib/server-fns.js`): + +``` +import { createServerFn } from "@tanstack/react-start"; +import { requireUser } from "@/lib/auth-middleware"; + +// requireUser rejects anonymous callers with 401: the connection belongs to the signed-in app user. +export const listMyEvents = createServerFn({ method: "GET" }) + .middleware([requireUser]) + .handler(async ({ context }) => { + // connectorId is the registered connector's id, e.g. "abc123" + const connection = await context.getBase44().asServiceRole.connectors + .getCurrentAppUserConnection(connectorId) + .catch((error) => { + if (error?.status === 404) return null; // this app user has not connected (or has revoked) the account + throw error; + }); + if (!connection) return { connected: false }; + + const response = await fetch('https://www.googleapis.com/calendar/v3/calendars/primary/events', { + headers: { 'Authorization': `Bearer ${connection.accessToken}` } + }); + if (!response.ok) throw new Error(`Google Calendar ${response.status}`); // a provider failure, not a disconnect + const data = await response.json(); + return { connected: true, events: data.items || [] }; // return data, never the token + }); +``` +</app_user_connector_sdk> + +<app_user_connector_frontend_rules> +**Three rules you MUST follow when generating frontend code that uses app user connectors. Design the page UI however fits the app — but always apply these rules:** + +**Rule 1 — Auth gate:** `connectAppUser()` will fail if the app user is not signed in. Use `base44.auth.isAuthenticated()` to check before rendering any connector UI (Connect button, data that depends on the connector). If not authenticated, show a login button that calls `base44.auth.redirectToLogin()`. + +**Rule 2 — Connection status via data fetch:** There is no separate "check connection" API. Instead, call the server function that uses the connector on page load. It returns `{ connected: false }` when the app user has not connected the account → show the Connect button; otherwise it returns the data. If it throws, something else failed (the provider errored, the network dropped) → show an error and keep the connection state — never send the user to reconnect for it. Extract this fetch as a reusable function — you will call it again in Rule 3. + +**Rule 3 — Auto-refresh after OAuth:** The OAuth flow opens in a new tab. When the user finishes and the tab closes, the original page doesn't know. To fix this: use `window.open()` to keep a reference to the popup, then poll `popup.closed` — when it closes, call the reusable fetch from Rule 2 to refresh data automatically. + +**How the three rules connect (hooks/handlers skeleton):** +``` +const [user, setUser] = useState(null); +const [data, setData] = useState(null); +const [connected, setConnected] = useState(false); +const [loading, setLoading] = useState(true); +const [error, setError] = useState(null); + +// Rule 2: reusable fetch — doubles as connection check AND data loader +const fetchData = async () => { + try { + const res = await listMyEvents(); // the server function, imported from "@/lib/server-fns" + setConnected(res.connected); + setData(res.connected ? res : null); + setError(null); + } catch (e) { + setError(e); // provider or network failure: show it, keep the connection state + } +}; + +// Rule 1+2: check auth first, then fetch to detect connection status +useEffect(() => { + base44.auth.isAuthenticated().then(async (authed) => { + if (authed) { + const me = await base44.auth.me(); + setUser(me); + await fetchData(); + } + setLoading(false); + }); +}, []); + +// Rule 3: open OAuth popup, poll for close, then re-fetch +const handleConnect = async () => { + const url = await base44.connectors.connectAppUser(CONNECTOR_ID); + const popup = window.open(url, "_blank"); + const timer = setInterval(() => { + if (!popup || popup.closed) { + clearInterval(timer); + fetchData(); + } + }, 500); +}; + +const handleDisconnect = async () => { + await base44.connectors.disconnectAppUser(CONNECTOR_ID); + setConnected(false); + setData(null); +}; + +// Then in your JSX, use loading, user, and connected to decide what to render. +// If !user, show a login button that calls base44.auth.redirectToLogin(). +// Design the UI however fits the app. +``` +</app_user_connector_frontend_rules> +</app_user_connectors> diff --git a/skills/base44-platform-frontend/references/web-spa.md b/skills/base44-platform-frontend/references/web-spa.md index 36c2b1d..386a64a 100644 --- a/skills/base44-platform-frontend/references/web-spa.md +++ b/skills/base44-platform-frontend/references/web-spa.md @@ -59,6 +59,7 @@ export default function Layout() { return <div><Sidebar /><main><Outlet /></main <routing_conventions> - Main/home page on "/" only — no second route under the component name (both "/" and "/Home"). - Changing the main page: the "/" route in src/App.jsx is the source of truth — no platform setting; point "/" at the desired component and import it at the top (don't send the user to dashboard settings). +- Unknown paths: keep `<Route path="*" element={<PageNotFound />} />` last — never point it at Home. Every not-found screen (that page, and a detail page whose item does not exist — never on a failed load) appends `<meta name="prerender-status-code" content="404">` to `document.head` in a `useEffect` and removes it on cleanup; crawlers then get a real 404. Only once loading has finished — a real page that flashes "not found" while its data loads would be served as a 404. </routing_conventions> Keep these framework wrappers in src/App.jsx intact — auth, data fetching, routing, and toasts run through them: diff --git a/skills/base44-platform-frontend/references/web-tanstack.md b/skills/base44-platform-frontend/references/web-tanstack.md index 4a93815..5e408b9 100644 --- a/skills/base44-platform-frontend/references/web-tanstack.md +++ b/skills/base44-platform-frontend/references/web-tanstack.md @@ -69,7 +69,7 @@ Write Tailwind classes as literal strings (const maps/arrays included) — the b <prebuilt_components> <root_document> -`src/routes/__root.jsx` renders the document and providers. Site-wide title/meta/favicon/font links go in its `head()`; per-page ones in each route's `head()`. External scripts (Analytics, AdSense) go in `<body>` next to `<Base44Scripts />`, never in `head()` — the preview proxy prepends its own scripts to `<head>`, which breaks hydration of any script rendered there. Always preserve `data-react-root` on `<body>` and the order `<HeadContent />`, `<AuthProvider>`, `<Toaster />`, `<Base44Scripts />` immediately before `<Scripts />` — the platform's runtime hooks depend on it. +`src/routes/__root.jsx` renders the document and providers. Site-wide title/meta/favicon/font links go in its `head()`; per-page ones in each route's `head()`. External scripts (Analytics, AdSense) go in `<body>` next to `<Base44Scripts />`, never in `head()`. Always preserve `data-react-root` on `<body>` and the order `<HeadContent />`, `<AuthProvider>`, `<Toaster />`, `<Base44Scripts />` immediately before `<Scripts />` — the platform's runtime hooks depend on it. </root_document> </prebuilt_components> diff --git a/skills/base44-platform-integrations-sdk/references/mobile.md b/skills/base44-platform-integrations-sdk/references/mobile.md index 12ca735..e0c846f 100644 --- a/skills/base44-platform-integrations-sdk/references/mobile.md +++ b/skills/base44-platform-integrations-sdk/references/mobile.md @@ -79,7 +79,7 @@ Here are the integration packages available in this app: <endpoint> <name>SendEmail</name> <description>Sends email. Reaches registered app users always; reaching an address that is not a registered user requires a paid plan and a custom domain connected to the app. Optionally specify the sender name; otherwise the app name is used.</description> - <input_schema>{'$defs': {'EmailAttachment': {'description': 'One attachment as a caller supplies it: a name plus exactly one source.\n\nNo caller-supplied content type: the extension is already constrained to the\nallowlist, so it determines the type and the two can never disagree.', 'properties': {'filename': {'description': 'Name the recipient sees, with an extension from the supported set: csv, docx, gif, ics, jpeg, jpg, md, pdf, png, txt, webp, xlsx', 'title': 'Filename', 'type': 'string'}, 'file_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "URL or storage URI of a file THIS app already uploaded (e.g. the file_url returned by UploadFile). Another app's files are refused.", 'title': 'File Url'}, 'content': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Base64-encoded bytes, for a document the function generated itself. Supply either this or file_url, never both.', 'title': 'Content'}}, 'required': ['filename'], 'title': 'EmailAttachment', 'type': 'object'}}, 'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'from_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'From Name'}, 'to': {'title': 'To', 'type': 'string'}, 'subject': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Subject line. Optional with a template: it overrides the template's own <title>.", 'title': 'Subject'}, 'body': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Body'}, 'html': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "HTML body — the same thing as 'body' under an explicit name. Set one or the other, never both.", 'title': 'Html'}, 'text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Plain-text body. On its own, sends a plain-text email. Alongside 'body'/'html', both go out as one multipart/alternative email and the recipient's mail client renders whichever it prefers — so the two must say the same thing. Not accepted with a template: templates are HTML-only.", 'title': 'Text'}, 'template_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Id'}, 'template_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Name'}, 'variables': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'type': 'null'}], 'default': None, 'title': 'Variables'}, 'attachments': {'anyOf': [{'items': {'$ref': '#/$defs/EmailAttachment'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Attachments'}}, 'required': ['to'], 'title': 'SendEmailModel', 'type': 'object'}</input_schema> + <input_schema>{'$defs': {'EmailAttachment': {'description': 'One attachment as a caller supplies it: a name plus exactly one source.\n\nNo caller-supplied content type: the extension is already constrained to the\nallowlist, so it determines the type and the two can never disagree.', 'properties': {'filename': {'description': 'Name the recipient sees, with an extension from the supported set: csv, docx, gif, ics, jpeg, jpg, md, pdf, png, txt, webp, xlsx', 'title': 'Filename', 'type': 'string'}, 'file_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "URL or storage URI of a file THIS app already uploaded (e.g. the file_url returned by UploadFile). Another app's files are refused.", 'title': 'File Url'}, 'content': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Base64-encoded bytes, for a document the function generated itself. Supply either this or file_url, never both.', 'title': 'Content'}}, 'required': ['filename'], 'title': 'EmailAttachment', 'type': 'object'}}, 'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'from_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'From Name'}, 'to': {'title': 'To', 'type': 'string'}, 'subject': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Subject line. Optional with a template: it overrides the template's own <title>.", 'title': 'Subject'}, 'body': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Body'}, 'html': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "HTML body — the same thing as 'body' under an explicit name. Set one or the other, never both.", 'title': 'Html'}, 'text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Plain-text body. On its own, sends a plain-text email. Alongside 'body'/'html', both go out as one multipart/alternative email and the recipient's mail client renders whichever it prefers — so the two must say the same thing. Not accepted with a template: templates are HTML-only.", 'title': 'Text'}, 'template_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Id'}, 'template_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Name'}, 'variables': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'type': 'null'}], 'default': None, 'title': 'Variables'}, 'attachments': {'anyOf': [{'items': {'$ref': '#/$defs/EmailAttachment'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Attachments'}, 'category': {'anyOf': [{'enum': ['marketing', 'transactional'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Category'}}, 'required': ['to'], 'title': 'SendEmailModel', 'type': 'object'}</input_schema> </endpoint> <endpoint> diff --git a/skills/base44-platform-integrations-sdk/references/web-spa.md b/skills/base44-platform-integrations-sdk/references/web-spa.md index aaf7801..8938252 100644 --- a/skills/base44-platform-integrations-sdk/references/web-spa.md +++ b/skills/base44-platform-integrations-sdk/references/web-spa.md @@ -73,7 +73,7 @@ Integration packages in this app: <endpoint> <name>SendEmail</name> <description>Sends email. Reaches registered app users always; reaching an address that is not a registered user requires a paid plan and a custom domain connected to the app. Optionally specify the sender name; otherwise the app name is used.</description> -<input_schema>{'$defs': {'EmailAttachment': {'description': 'One attachment as a caller supplies it: a name plus exactly one source.\n\nNo caller-supplied content type: the extension is already constrained to the\nallowlist, so it determines the type and the two can never disagree.', 'properties': {'filename': {'description': 'Name the recipient sees, with an extension from the supported set: csv, docx, gif, ics, jpeg, jpg, md, pdf, png, txt, webp, xlsx', 'title': 'Filename', 'type': 'string'}, 'file_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "URL or storage URI of a file THIS app already uploaded (e.g. the file_url returned by UploadFile). Another app's files are refused.", 'title': 'File Url'}, 'content': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Base64-encoded bytes, for a document the function generated itself. Supply either this or file_url, never both.', 'title': 'Content'}}, 'required': ['filename'], 'title': 'EmailAttachment', 'type': 'object'}}, 'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'from_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'From Name'}, 'to': {'title': 'To', 'type': 'string'}, 'subject': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Subject line. Optional with a template: it overrides the template's own <title>.", 'title': 'Subject'}, 'body': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Body'}, 'html': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "HTML body — the same thing as 'body' under an explicit name. Set one or the other, never both.", 'title': 'Html'}, 'text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Plain-text body. On its own, sends a plain-text email. Alongside 'body'/'html', both go out as one multipart/alternative email and the recipient's mail client renders whichever it prefers — so the two must say the same thing. Not accepted with a template: templates are HTML-only.", 'title': 'Text'}, 'template_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Id'}, 'template_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Name'}, 'variables': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'type': 'null'}], 'default': None, 'title': 'Variables'}, 'attachments': {'anyOf': [{'items': {'$ref': '#/$defs/EmailAttachment'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Attachments'}}, 'required': ['to'], 'title': 'SendEmailModel', 'type': 'object'}</input_schema> +<input_schema>{'$defs': {'EmailAttachment': {'description': 'One attachment as a caller supplies it: a name plus exactly one source.\n\nNo caller-supplied content type: the extension is already constrained to the\nallowlist, so it determines the type and the two can never disagree.', 'properties': {'filename': {'description': 'Name the recipient sees, with an extension from the supported set: csv, docx, gif, ics, jpeg, jpg, md, pdf, png, txt, webp, xlsx', 'title': 'Filename', 'type': 'string'}, 'file_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "URL or storage URI of a file THIS app already uploaded (e.g. the file_url returned by UploadFile). Another app's files are refused.", 'title': 'File Url'}, 'content': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Base64-encoded bytes, for a document the function generated itself. Supply either this or file_url, never both.', 'title': 'Content'}}, 'required': ['filename'], 'title': 'EmailAttachment', 'type': 'object'}}, 'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'from_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'From Name'}, 'to': {'title': 'To', 'type': 'string'}, 'subject': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Subject line. Optional with a template: it overrides the template's own <title>.", 'title': 'Subject'}, 'body': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Body'}, 'html': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "HTML body — the same thing as 'body' under an explicit name. Set one or the other, never both.", 'title': 'Html'}, 'text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Plain-text body. On its own, sends a plain-text email. Alongside 'body'/'html', both go out as one multipart/alternative email and the recipient's mail client renders whichever it prefers — so the two must say the same thing. Not accepted with a template: templates are HTML-only.", 'title': 'Text'}, 'template_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Id'}, 'template_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Name'}, 'variables': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'type': 'null'}], 'default': None, 'title': 'Variables'}, 'attachments': {'anyOf': [{'items': {'$ref': '#/$defs/EmailAttachment'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Attachments'}, 'category': {'anyOf': [{'enum': ['marketing', 'transactional'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Category'}}, 'required': ['to'], 'title': 'SendEmailModel', 'type': 'object'}</input_schema> </endpoint> <endpoint> diff --git a/skills/base44-platform-integrations-sdk/references/web-tanstack.md b/skills/base44-platform-integrations-sdk/references/web-tanstack.md index 475321f..300c268 100644 --- a/skills/base44-platform-integrations-sdk/references/web-tanstack.md +++ b/skills/base44-platform-integrations-sdk/references/web-tanstack.md @@ -71,7 +71,7 @@ Integration packages in this app: <endpoint> <name>SendEmail</name> <description>Sends email. Reaches registered app users always; reaching an address that is not a registered user requires a paid plan and a custom domain connected to the app. Optionally specify the sender name; otherwise the app name is used.</description> -<input_schema>{'$defs': {'EmailAttachment': {'description': 'One attachment as a caller supplies it: a name plus exactly one source.\n\nNo caller-supplied content type: the extension is already constrained to the\nallowlist, so it determines the type and the two can never disagree.', 'properties': {'filename': {'description': 'Name the recipient sees, with an extension from the supported set: csv, docx, gif, ics, jpeg, jpg, md, pdf, png, txt, webp, xlsx', 'title': 'Filename', 'type': 'string'}, 'file_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "URL or storage URI of a file THIS app already uploaded (e.g. the file_url returned by UploadFile). Another app's files are refused.", 'title': 'File Url'}, 'content': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Base64-encoded bytes, for a document the function generated itself. Supply either this or file_url, never both.', 'title': 'Content'}}, 'required': ['filename'], 'title': 'EmailAttachment', 'type': 'object'}}, 'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'from_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'From Name'}, 'to': {'title': 'To', 'type': 'string'}, 'subject': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Subject line. Optional with a template: it overrides the template's own <title>.", 'title': 'Subject'}, 'body': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Body'}, 'html': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "HTML body — the same thing as 'body' under an explicit name. Set one or the other, never both.", 'title': 'Html'}, 'text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Plain-text body. On its own, sends a plain-text email. Alongside 'body'/'html', both go out as one multipart/alternative email and the recipient's mail client renders whichever it prefers — so the two must say the same thing. Not accepted with a template: templates are HTML-only.", 'title': 'Text'}, 'template_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Id'}, 'template_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Name'}, 'variables': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'type': 'null'}], 'default': None, 'title': 'Variables'}, 'attachments': {'anyOf': [{'items': {'$ref': '#/$defs/EmailAttachment'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Attachments'}}, 'required': ['to'], 'title': 'SendEmailModel', 'type': 'object'}</input_schema> +<input_schema>{'$defs': {'EmailAttachment': {'description': 'One attachment as a caller supplies it: a name plus exactly one source.\n\nNo caller-supplied content type: the extension is already constrained to the\nallowlist, so it determines the type and the two can never disagree.', 'properties': {'filename': {'description': 'Name the recipient sees, with an extension from the supported set: csv, docx, gif, ics, jpeg, jpg, md, pdf, png, txt, webp, xlsx', 'title': 'Filename', 'type': 'string'}, 'file_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "URL or storage URI of a file THIS app already uploaded (e.g. the file_url returned by UploadFile). Another app's files are refused.", 'title': 'File Url'}, 'content': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Base64-encoded bytes, for a document the function generated itself. Supply either this or file_url, never both.', 'title': 'Content'}}, 'required': ['filename'], 'title': 'EmailAttachment', 'type': 'object'}}, 'properties': {'app_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Id'}, 'app_owner': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'App Owner'}, 'from_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'From Name'}, 'to': {'title': 'To', 'type': 'string'}, 'subject': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Subject line. Optional with a template: it overrides the template's own <title>.", 'title': 'Subject'}, 'body': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Body'}, 'html': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "HTML body — the same thing as 'body' under an explicit name. Set one or the other, never both.", 'title': 'Html'}, 'text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Plain-text body. On its own, sends a plain-text email. Alongside 'body'/'html', both go out as one multipart/alternative email and the recipient's mail client renders whichever it prefers — so the two must say the same thing. Not accepted with a template: templates are HTML-only.", 'title': 'Text'}, 'template_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Id'}, 'template_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Template Name'}, 'variables': {'anyOf': [{'additionalProperties': True, 'type': 'object'}, {'type': 'null'}], 'default': None, 'title': 'Variables'}, 'attachments': {'anyOf': [{'items': {'$ref': '#/$defs/EmailAttachment'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Attachments'}, 'category': {'anyOf': [{'enum': ['marketing', 'transactional'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'title': 'Category'}}, 'required': ['to'], 'title': 'SendEmailModel', 'type': 'object'}</input_schema> </endpoint> <endpoint> diff --git a/skills/base44-platform-manifest.json b/skills/base44-platform-manifest.json index 9864136..c23e920 100644 --- a/skills/base44-platform-manifest.json +++ b/skills/base44-platform-manifest.json @@ -4,36 +4,36 @@ "skills/base44-platform-actors/references/mobile.md": "31cc9bf4dcb7aacc9e8f98df8020c86130d0c14f719518945d6613d29798dbfe", "skills/base44-platform-actors/references/web.md": "de7d5e7aea53b8a9e1f4eb89a75298d49a72da1b923215e87779926c6b7cf655", "skills/base44-platform-agents/SKILL.md": "1d6f55d6bd71449d629b2662c647e8e242bdb0ab6432c33d34534437241888ab", - "skills/base44-platform-agents/references/web-spa.md": "d429d967bd32996e597ce1386a8d255972f79080545536b8e9a5bca2322048c0", - "skills/base44-platform-agents/references/web-tanstack.md": "e48e0c5e2373ddcad47c02722b4b5dc5971640536b30e99896ef5113f97f4d74", + "skills/base44-platform-agents/references/web-spa.md": "25e0031b1556fc6ddfa9871f472a82acf6460033ccec0510090cd70343eb8c73", + "skills/base44-platform-agents/references/web-tanstack.md": "94c1b47f873f44daebdb46cbad6931e4ac769dd1669a237aa36e808154f8e0d9", "skills/base44-platform-backend-functions/SKILL.md": "146505a402ba4ba478f4042dfbf2e6d46e3497fa73e9102ab8404c76224a3376", - "skills/base44-platform-backend-functions/references/mobile.md": "034fe28f812897363b91242078a1092943e319fdee0fc28ea8d2fd779deb8498", - "skills/base44-platform-backend-functions/references/web-spa.md": "43620240a07f0e0b776ebb7c38945fddecd806a23a963b7b3ec5fd2feeecbfaa", - "skills/base44-platform-backend-functions/references/web-tanstack.md": "960f4fede0d0c878e8e98b23c196ac9fcc99a7b9e2ef8f46cf005d607e73f0cd", + "skills/base44-platform-backend-functions/references/mobile.md": "d9dbe5d90c1c00b126bf4981605edefd1acac9308c0e665fc1b1d4dae17782af", + "skills/base44-platform-backend-functions/references/web-spa.md": "777b39518159c3241fc385b6506debdf2d3486d69a1d131fe2df615ce366dc6f", + "skills/base44-platform-backend-functions/references/web-tanstack.md": "966a9b0495d78b44acf64eca0bd00bc1def50258dc71389c752ee62ede80a697", "skills/base44-platform-connectors/SKILL.md": "e037d9536b1d8267b85a7d25ab0416c1a8fb0f88763e2365ebae97cec23a4bea", - "skills/base44-platform-connectors/references/mobile.md": "7cb1dbad4829a203a0dc44bde8646410e3484c9fb45c64de0f7201d870e5aefd", - "skills/base44-platform-connectors/references/web-spa.md": "aa905eab8a95f97c85e4755123eae2b38c5e4e62a09c516d481519b51d04734c", - "skills/base44-platform-connectors/references/web-tanstack.md": "cec2fb0c272335ccb605f774bd3f8b621b37d3f8985c1e9ff4c56857a3d6b209", + "skills/base44-platform-connectors/references/mobile.md": "0b06a72e4152209a652dbeeeacf9cc2577391851e9a7437deb6de8b698c48ddb", + "skills/base44-platform-connectors/references/web-spa.md": "cd74ec5f30b0f50adc06328b5f7514c59dc01c277de57835473323535a6ffb9d", + "skills/base44-platform-connectors/references/web-tanstack.md": "5e40af077a22f09d040cb64a3069c52d0b463cc0b6594b0d5effb3c8944c63c9", "skills/base44-platform-entities/SKILL.md": "9b37a2aff19212a88aa0c14b113a3246c9ae714a8b45247f0c2e7c251d0c9466", "skills/base44-platform-entities/references/mobile.md": "cbee462933515db5329f9901ff401d666f9084b7bb1a5ca91c90be6b33101240", "skills/base44-platform-entities/references/rls.md": "a87a71751bb63c28a0629c2ca65019c726c3b549149a029f74f5a84cb86b8dcf", "skills/base44-platform-entities/references/web.md": "e30672b094b3b5ed2fc926b06ee3646a8f1777491cae41cfb9a0be65f8bb1a9b", "skills/base44-platform-frontend/SKILL.md": "0b06d58d67108e260d147d1ced4143755a451823e59e6a3012cfd71556921ac7", "skills/base44-platform-frontend/references/mobile.md": "ecf9499821d8cf345e8b32143f2d0f3e72a4daeda06588a251d26f546bcac60a", - "skills/base44-platform-frontend/references/web-spa.md": "410aee9583f1cf24dead07d4429d27ca7063e38053d50aa1cf7bc2a88305c56b", - "skills/base44-platform-frontend/references/web-tanstack.md": "5c4227b1fc2fa80f2da40dff60e68f67f8c0054cdbf7758bc43bb6cea49d6614", + "skills/base44-platform-frontend/references/web-spa.md": "56ce2d10899f72cc50e2ed0f55a60520d96ce7e897b7428bc640c3790ab34e46", + "skills/base44-platform-frontend/references/web-tanstack.md": "6aa5a8dc515553055d29bb33a663731bf8d3465fd3a717ce458ca874a243ab9a", "skills/base44-platform-integrations-sdk/SKILL.md": "cc18027267ab6f7c64d34e8d26d3aa4b3fcfe2d6c35b2c81468a2351c6658997", - "skills/base44-platform-integrations-sdk/references/mobile.md": "234428eb01d45bd67569840dd32404018f1b60efacddd98908798cc57e7cc843", - "skills/base44-platform-integrations-sdk/references/web-spa.md": "6f384fbd1ce1eb822fddd55503053af25d1ecce709ed3988ec45efbabba0d232", - "skills/base44-platform-integrations-sdk/references/web-tanstack.md": "c7baee1ab96aac7196753baa8106d597711662b6c87149a60412667b34b197e0", + "skills/base44-platform-integrations-sdk/references/mobile.md": "8dfc2de889e3a54e1c0de4dd32daf83b3dee5984d23a04d11c491a0b83807a1c", + "skills/base44-platform-integrations-sdk/references/web-spa.md": "5963ff6747e9a8b5224e448c1da76d54da9fb42f6344c044ed25c973daadf950", + "skills/base44-platform-integrations-sdk/references/web-tanstack.md": "d0a361ac1d5dcca09ce3e87725c86526181ad3f9587e6ea0f71136fc905657ed", "skills/base44-platform-platform/SKILL.md": "cdcec44e4885c6970cc6f79880194fbe7dd99c2aedad9480d4afca18ce339a19", "skills/base44-platform-platform/references/mobile.md": "bc5116e7e7896f56908dd8e400cb7b7101f0ee53597725a0af614002b1ac669a", "skills/base44-platform-platform/references/web-spa.md": "52917f4b90d778d21e73b70ab7862e9250044831b51db4dd8723cf68a6706941", "skills/base44-platform-platform/references/web-tanstack.md": "625c2c6f75647c843292b9a864db8b737b674848cae1d9e44e9630185c8429d9", "skills/base44-platform-workflows/SKILL.md": "fbe451b408f73831a830094c3968ae15ef27f846138808c54039ef24f7dc56c4", - "skills/base44-platform-workflows/references/automations-legacy.md": "0518e4aaab82d2e2ff842fce5eb7daeb140900eb48a952a3528e7cc84e2db502", + "skills/base44-platform-workflows/references/automations-legacy.md": "4a4d8932a4f544a29260e4095eb7e98cb68b5dd229c7f9066579fdce7bbf9ae3", "skills/base44-platform-workflows/references/backend-functions.md": "92307752be105d6db7e13576740785281e1812dec508f66a2b530312973b71f3", - "skills/base44-platform-workflows/references/mobile-automations.md": "976a3adca97c0023352bccdefa36a6330a90e4db48d67aa3694f0dedf646de67", + "skills/base44-platform-workflows/references/mobile-automations.md": "20e412446eb1190ae33837799e31090ad2cf3b254a2675ca7ed22ff9cc4ac907", "skills/base44-platform-workflows/references/web-tanstack.md": "679d1a60b5854952426f08f71001dd8e962ffd1a15e2c514a2f88c4045b848f7" } } diff --git a/skills/base44-platform-workflows/references/automations-legacy.md b/skills/base44-platform-workflows/references/automations-legacy.md index 0e4041b..0a59fe9 100644 --- a/skills/base44-platform-workflows/references/automations-legacy.md +++ b/skills/base44-platform-workflows/references/automations-legacy.md @@ -5,6 +5,7 @@ <automations_capability> Automations run backend functions automatically — on a schedule, entity record changes, connector webhook events, or app users starting in-app agent conversations. +Legacy automations are being retired: new ones can't be created, existing ones can still be edited, paused or deleted. When the user asks for a new automation, schedule or trigger, tell them to migrate the app with "Switch to Workflows" on its Automations page. <user_timezone> Times the user mentions ("9am", "midnight") are local to them — ask for their timezone when it is unknown: @@ -13,7 +14,7 @@ Times the user mentions ("9am", "midnight") are local to them — ask for their </user_timezone> <managing_automations> -This app runs the legacy automations pipeline (app profile `automation_pipeline: legacy_automations`). Automations are not files: create, update, toggle and delete them in the dashboard (Automations) or through the platform API (the `automations` area of the API catalog). The parameters below are the ones those surfaces accept, named as the builder passes them. +This app runs the legacy automations pipeline (app profile `automation_pipeline: legacy_automations`). Automations are not files: update, toggle and delete the existing ones in the dashboard (Automations); new ones can't be created. A new schedule or trigger means migrating the app with "Switch to Workflows" on its Automations page, after which this skill's workflows reference applies. The parameters below are the ones the dashboard accepts, named as the builder passes them. </managing_automations> <scheduled_automations> @@ -22,7 +23,6 @@ schedule_type="simple": repeat_interval=5, repeat_unit="minutes" | repeat_interv schedule_type="cron": standard "minute hour day-of-month month day-of-week" — "*/5 * * * *" = every 5 minutes, "0 9 * * 1-5" = 9am weekdays. schedule_mode="one-time": one_time_date="2024-01-15T10:00:00". Ends: ends_type="never" (default) | "on" + ends_on_date="2024-12-31T23:59:59" | "after" + ends_after_count=10. - </scheduled_automations> <entity_automations> @@ -31,19 +31,19 @@ Function payload: event { type, entity_name, entity_id }; data: current entity d Optional trigger_conditions pre-filter entity events. Conditions evaluate against a wrapped object (entity automations only, not connector): {"data": {...current...}, "old_data": {...previous, update only...}, "changed_fields": [...]}. Field paths take the wrapping prefix: "data.<field>", "old_data.<field>", or "changed_fields" with operator "contains" to test whether a field changed. +Example (only when status changes to "completed"): +trigger_conditions={"logic": "and", "conditions": [{"field": "changed_fields", "operator": "contains", "value": "status"}, {"field": "data.status", "operator": "equals", "value": "completed"}]} </entity_automations> <in_app_agent_automations> -automation_type="in_app_agent" — fires when an app user starts a new in-app agent conversation. Use it to react to conversation starts / first messages — never a scheduled poll. -Required: function_name. Optional: agent_name (exact configured name; omit on create = all agents; "" on update clears), event_types (currently only "conversation_started"). +automation_type="in_app_agent" — fires when an app user starts a new in-app agent conversation. +Fields: function_name, agent_name (exact configured name; "" clears = all agents), event_types (currently only "conversation_started"). Function payload: automation { id, type, name }; event { type, agent_name, conversation_id, message_id, channel }; message { text, has_files }; app_user { id }; payload_too_large (message text omitted as too large). - </in_app_agent_automations> <connector_automations> automation_type="connector" — fires on a connected service's webhook events (new calendar event, Drive file change, …). -Required: integration_type (e.g. "googlecalendar", "googledrive"), events (array of event names), function_name. Optional: resource_id (Google Drive/Calendar — specific file/calendar to watch). -Prerequisites: connector authorized (see the `base44-platform-connectors` skill); handler function created first. Provider payloads/auth differ — read the connector's usage guide (`list_connectors` with the integration type) before writing the function. +Fields: integration_type (e.g. "googlecalendar", "googledrive"), events (array of event names), function_name. Provider payloads/auth differ — read the provider's own webhook documentation for payload and auth before editing the handler (`list_connectors` gives the integration type, scopes and connection state, not a handler guide). Optional trigger_conditions pre-filter webhook events: {"logic": "and", "conditions": [{"field": "<dot.path>", "operator": "<op>", "value": "<target>"}]} - field: dot-path into the provider's raw webhook payload — no "data." prefix (unlike entity conditions); Slack: "event.channel", "event.text", "event.user". Take paths from the provider's public webhook payload docs. @@ -52,9 +52,7 @@ Optional trigger_conditions pre-filter webhook events: {"logic": "and", "conditi </connector_automations> <usage_guidelines> -- To react to external events, prefer a connector automation (real-time webhooks) over a scheduled poll; check webhook support in the connector's details, poll only if no webhook-capable connector exists. -- Create the backend function before the automation; authorize connectors first. -- Check existing automations (dashboard or API) before adding one; descriptive names. -- Pause/resume from the dashboard or API. +- Existing automations and their IDs: the dashboard (Automations). +- Pause/resume from the dashboard. </usage_guidelines> </automations_capability> diff --git a/skills/base44-platform-workflows/references/mobile-automations.md b/skills/base44-platform-workflows/references/mobile-automations.md index c6bcd7d..66c7853 100644 --- a/skills/base44-platform-workflows/references/mobile-automations.md +++ b/skills/base44-platform-workflows/references/mobile-automations.md @@ -5,6 +5,7 @@ <automations_capability> You can manage automations to run backend functions automatically - on a schedule, when entity records change, when external services send webhook events, or when app users start in-app agent conversations. +Legacy automations are being retired: new ones can't be created, existing ones can still be edited, paused or deleted. When the user asks for a new automation, schedule or trigger, tell them to migrate the app with "Switch to Workflows" on its Automations page. <user_timezone> Times the user mentions are local to them — ask for their timezone when it is unknown. @@ -15,7 +16,7 @@ IMPORTANT: When the user mentions times (e.g., "9am", "2pm", "midnight"), they m </user_timezone> <managing_automations> -Automations are not files: create, update, toggle and delete them in the dashboard (Automations) or through the platform API (the `automations` area of the API catalog). The parameters below are the ones those surfaces accept, named as the builder passes them. +Automations are not files: update, toggle and delete the existing ones in the dashboard (Automations); new ones can't be created. A new schedule or trigger means migrating the app with "Switch to Workflows" on its Automations page, after which the `base44-platform-workflows` skill's workflows reference applies. The parameters below are the ones the dashboard accepts, named as the builder passes them. </managing_automations> <automation_types> @@ -47,12 +48,6 @@ Run a function when entity records are created, updated, or deleted. Required: entity_name, event_types (array of: "create", "update", "delete") -Examples: -- Send email on new order: entity_name="orders", event_types=["create"] -- Track status changes: entity_name="orders", event_types=["update"] -- Cleanup on delete: entity_name="users", event_types=["delete"] -- Multiple events: entity_name="orders", event_types=["create", "update"] - The function receives a payload with: - event: { type, entity_name, entity_id } - data: current entity data (null if payload_too_large is true) @@ -87,10 +82,7 @@ trigger_conditions={ **In-App Agent Automations (automation_type="in_app_agent"):** Run a function when an app user starts a new conversation with an in-app agent. -Required: function_name -Optional: agent_name (exact configured agent name; omit on create for all in-app agents; pass "" on update to clear a specific agent), event_types (array; currently only "conversation_started") - -Use this trigger when the user wants to react to app-user conversations in an in-app agent. Do NOT implement this with scheduled polling. +Fields: function_name, agent_name (exact configured agent name; pass "" to clear a specific agent and watch all), event_types (array; currently only "conversation_started") The function receives a payload with: - automation: { id, type, name } @@ -99,20 +91,12 @@ The function receives a payload with: - app_user: { id } - payload_too_large: true if the message text was omitted because it was too large -Example: -- Notify on a new support chat: agent_name="Support", event_types=["conversation_started"] - **Connector Automations (automation_type="connector"):** Run a function when a connected external service sends a webhook event (e.g., new calendar event, Google Drive file change). -Required: integration_type (e.g., "googlecalendar", "googledrive"), events (array of event names), function_name -Optional: resource_id (for Google Drive/Calendar - specific file or calendar to watch) - -Prerequisites: -- The connector must be authorized first (see the `base44-platform-connectors` skill) -- The backend function to handle the webhook must exist before creating the automation +Fields: integration_type (e.g., "googlecalendar", "googledrive"), events (array of event names), function_name -Each provider has a different payload structure and auth model. Always read the connector's usage guide (`list_connectors` with the integration type) BEFORE writing the function. +Each provider has a different payload structure and auth model. Always read the provider's own webhook documentation for its payload and auth model (`list_connectors` gives the integration type, scopes and connection state, not a handler guide) BEFORE editing the handler function. Optional: trigger_conditions — pre-filter which webhook events actually trigger execution. Use when the user only wants to act on a subset of events (e.g., a specific Slack channel, a specific Drive folder). @@ -133,25 +117,9 @@ trigger_conditions format: </automation_types> <usage_guidelines> -Webhook vs polling: -When the user wants their app to react to external events (calendar changes, new messages, CRM updates, etc.), -Try using a connector automation (automation_type="connector") instead of a scheduled poll. -Connector automations deliver events in real time via webhooks and are more efficient and responsive than polling on a schedule. -Only fall back to scheduled polling when there is no connector with webhook support for the service in question. -Check the connector's details to see whether it supports webhooks before choosing scheduled polling. - -In-app agent conversation triggers: -When the user wants their app to react to a user starting or sending the first message in an in-app agent conversation, -use an in-app agent automation (automation_type="in_app_agent") with event_types=["conversation_started"]. -Do not create a scheduled automation that checks for recent conversations. - -Other guidelines: -1. First, ensure the backend function exists before creating an automation for it -2. For connector automations, ensure the connector is authorized before creating the automation -3. Check existing automations in the dashboard or API before adding one -4. Use descriptive names for automations to make them easy to identify -5. Pause/resume automations from the dashboard or API -6. Minimum interval for scheduled automations is 5 minutes +1. Find existing automations and their IDs in the dashboard (Automations) +2. Pause/resume automations from the dashboard +3. Minimum interval for scheduled automations is 5 minutes </usage_guidelines> </automations_capability> From 38a07cd7b19b628586ab6b272d7bfb189162ecab Mon Sep 17 00:00:00 2001 From: Carmel Cohen <carmelc@wix.com> Date: Sun, 1 Nov 2026 11:29:51 +0000 Subject: [PATCH 15/15] docs: the human-merge label in CONTRIBUTING Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2fHMwsHuKC2HxRkojwH2X --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b5748da..ab44915 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -139,7 +139,7 @@ the in-editor AI learns it, one reference per app shape. 3. `Auto-merge compiled skill updates` enables squash auto-merge on a labeled PR opened by `base44-github-actions[bot]` whose diff is confined to `skills/base44-platform-*/**` and `SOURCE_COMMIT`. A PR published on an engineer's approval after review findings waits for a - human merge; so does everything else. The Slack notifier posts the PR like any other skill-sync PR. + human merge; so does everything else (it carries the `human-merge` label, which the job skips). The Slack notifier posts the PR like any other skill-sync PR. ### How to change a compiled skill